mailkick 0.1.4 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -3,11 +3,19 @@ module Mailkick
3
3
  isolate_namespace Mailkick
4
4
 
5
5
  initializer "mailkick" do |app|
6
- Mailkick.discover_services
7
- secrets = app.respond_to?(:secrets) ? app.secrets : app.config
8
- Mailkick.secret_token ||= secrets.respond_to?(:secret_key_base) ? secrets.secret_key_base : secrets.secret_token
9
- ActiveSupport.on_load :action_mailer do
10
- helper Mailkick::UrlHelper
6
+ Mailkick.discover_services unless Mailkick.services.any?
7
+
8
+ Mailkick.secret_token ||= begin
9
+ creds =
10
+ if app.respond_to?(:credentials) && app.credentials.secret_key_base
11
+ app.credentials
12
+ elsif app.respond_to?(:secrets)
13
+ app.secrets
14
+ else
15
+ app.config
16
+ end
17
+
18
+ creds.respond_to?(:secret_key_base) ? creds.secret_key_base : creds.secret_token
11
19
  end
12
20
  end
13
21
  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(options = {})
4
- email_key = options[: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
@@ -0,0 +1,47 @@
1
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SESV2/Client.html
2
+
3
+ module Mailkick
4
+ class Service
5
+ class AwsSes < Mailkick::Service
6
+ REASONS_MAP = {
7
+ "BOUNCE" => "bounce",
8
+ "COMPLAINT" => "spam"
9
+ }
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def opt_outs
16
+ response = client.list_suppressed_destinations({
17
+ reasons: ["BOUNCE", "COMPLAINT"],
18
+ # TODO make configurable
19
+ start_date: Time.now - (86400 * 365),
20
+ end_date: Time.now
21
+ })
22
+
23
+ opt_outs = []
24
+ response.each do |page|
25
+ page.suppressed_destination_summaries.each do |record|
26
+ opt_outs << {
27
+ email: record.email_address,
28
+ time: record.last_update_time,
29
+ reason: REASONS_MAP[record.reason]
30
+ }
31
+ end
32
+ end
33
+ opt_outs
34
+ end
35
+
36
+ def self.discoverable?
37
+ !!defined?(::Aws::SESV2::Client)
38
+ end
39
+
40
+ private
41
+
42
+ def client
43
+ @client ||= ::Aws::SESV2::Client.new
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,7 +4,7 @@ module Mailkick
4
4
  class Service
5
5
  class Mailchimp < Mailkick::Service
6
6
  def initialize(options = {})
7
- @gibbon = ::Gibbon::API.new(options[:api_key] || ENV["MAILCHIMP_API_KEY"])
7
+ @gibbon = ::Gibbon::Request.new(api_key: options[:api_key] || ENV["MAILCHIMP_API_KEY"])
8
8
  @list_id = options[:list_id] || ENV["MAILCHIMP_LIST_ID"]
9
9
  end
10
10
 
@@ -14,11 +14,11 @@ module Mailkick
14
14
  end
15
15
 
16
16
  def unsubscribes
17
- fetch(@gibbon.lists.members(id: @list_id, status: "unsubscribed"), "unsubscribe")
17
+ fetch(@gibbon.lists(@list_id).members.retrieve(params: {status: "unsubscribed"}).body["members"], "unsubscribe")
18
18
  end
19
19
 
20
20
  def spam_reports
21
- fetch(@gibbon.lists.abuse_reports(id: @list_id), "spam")
21
+ fetch(@gibbon.lists(@list_id).abuse_reports.retrieve.body["abuse_reports"], "spam")
22
22
  end
23
23
 
24
24
  def self.discoverable?
@@ -28,9 +28,9 @@ module Mailkick
28
28
  protected
29
29
 
30
30
  def fetch(response, reason)
31
- response["data"].map do |record|
31
+ response.map do |record|
32
32
  {
33
- email: record["email"],
33
+ email: record["email_address"],
34
34
  time: ActiveSupport::TimeZone["UTC"].parse(record["timestamp_opt"] || record["date"]),
35
35
  reason: reason
36
36
  }
@@ -6,7 +6,7 @@ module Mailkick
6
6
  def initialize(options = {})
7
7
  require "mailgun"
8
8
  mailgun_client = ::Mailgun::Client.new(options[:api_key] || ENV["MAILGUN_API_KEY"])
9
- domain = options[:domain] || ActionMailer::Base.default_url_options[:host]
9
+ domain = options[:domain] || ActionMailer::Base.smtp_settings[:domain]
10
10
  @mailgun_events = ::Mailgun::Events.new(mailgun_client, domain)
11
11
  end
12
12
 
@@ -10,9 +10,12 @@ module Mailkick
10
10
  "unsub" => "unsubscribe"
11
11
  }
12
12
 
13
+ # TODO remove ENV["MANDRILL_APIKEY"]
13
14
  def initialize(options = {})
14
15
  require "mandrill"
15
- @mandrill = ::Mandrill::API.new(options[:api_key] || ENV["MANDRILL_APIKEY"])
16
+ @mandrill = ::Mandrill::API.new(
17
+ options[:api_key] || ENV["MANDRILL_APIKEY"] || ENV["MANDRILL_API_KEY"]
18
+ )
16
19
  end
17
20
 
18
21
  # TODO paginate
@@ -26,8 +29,9 @@ module Mailkick
26
29
  end
27
30
  end
28
31
 
32
+ # TODO remove ENV["MANDRILL_APIKEY"]
29
33
  def self.discoverable?
30
- !!(defined?(::Mandrill::API) && ENV["MANDRILL_APIKEY"])
34
+ !!(defined?(::Mandrill::API) && (ENV["MANDRILL_APIKEY"] || ENV["MANDRILL_API_KEY"]))
31
35
  end
32
36
  end
33
37
  end
@@ -0,0 +1,41 @@
1
+ # https://github.com/wildbit/postmark-gem
2
+
3
+ module Mailkick
4
+ class Service
5
+ class Postmark < Mailkick::Service
6
+ REASONS_MAP = {
7
+ "SpamNotification" => "spam",
8
+ "SpamComplaint" => "spam",
9
+ "Unsubscribe" => "unsubscribe",
10
+ }
11
+
12
+ def initialize(options = {})
13
+ @client = ::Postmark::ApiClient.new(options[:api_key] || ENV["POSTMARK_API_KEY"])
14
+ end
15
+
16
+ def opt_outs
17
+ bounces
18
+ end
19
+
20
+ def bounces
21
+ fetch(@client.bounces)
22
+ end
23
+
24
+ def self.discoverable?
25
+ !!(defined?(::Postmark) && ENV["POSTMARK_API_KEY"])
26
+ end
27
+
28
+ protected
29
+
30
+ def fetch(response)
31
+ response.map do |record|
32
+ {
33
+ email: record[:email],
34
+ time: ActiveSupport::TimeZone["UTC"].parse(record[:bounced_at]),
35
+ reason: REASONS_MAP.fetch(record[:type], "bounce")
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Mailkick
4
4
  class Service
5
- class Sendgrid < Mailkick::Service
5
+ class SendGrid < Mailkick::Service
6
6
  def initialize(options = {})
7
7
  @api_user = options[:api_user] || ENV["SENDGRID_USERNAME"]
8
8
  @api_key = options[:api_key] || ENV["SENDGRID_PASSWORD"]
@@ -41,5 +41,8 @@ module Mailkick
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ # backwards compatibility
46
+ Sendgrid = SendGrid
44
47
  end
45
48
  end
@@ -0,0 +1,52 @@
1
+ # https://github.com/sendgrid/sendgrid-ruby
2
+
3
+ module Mailkick
4
+ class Service
5
+ class SendGridV2 < Mailkick::Service
6
+ def initialize(options = {})
7
+ @api_key = options[:api_key] || ENV["SENDGRID_API_KEY"]
8
+ end
9
+
10
+ def opt_outs
11
+ unsubscribes + spam_reports + bounces
12
+ end
13
+
14
+ def unsubscribes
15
+ fetch(client.suppression.unsubscribes, "unsubscribe")
16
+ end
17
+
18
+ def spam_reports
19
+ fetch(client.suppression.spam_reports, "spam")
20
+ end
21
+
22
+ def bounces
23
+ fetch(client.suppression.bounces, "bounce")
24
+ end
25
+
26
+ def self.discoverable?
27
+ !!(defined?(::SendGrid::API) && ENV["SENDGRID_API_KEY"])
28
+ end
29
+
30
+ protected
31
+
32
+ def client
33
+ @client ||= ::SendGrid::API.new(api_key: @api_key).client
34
+ end
35
+
36
+ def fetch(query, reason)
37
+ # TODO paginate
38
+ response = query.get
39
+
40
+ raise "Bad status code: #{response.status_code}" if response.status_code.to_i != 200
41
+
42
+ response.parsed_body.map do |record|
43
+ {
44
+ email: record[:email],
45
+ time: Time.at(record[:created]),
46
+ reason: reason
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,21 +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
- true
4
+ Mailkick.process_opt_outs_method.call(opt_outs)
19
5
  end
20
6
  end
21
7
  end
@@ -0,0 +1,8 @@
1
+ module Mailkick
2
+ module UrlHelper
3
+ def mailkick_unsubscribe_url(subscriber, list, **options)
4
+ token = Mailkick.generate_token(subscriber, list)
5
+ mailkick.unsubscribe_subscription_url(token, **options)
6
+ end
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module Mailkick
2
- VERSION = "0.1.4"
2
+ VERSION = "1.0.1"
3
3
  end
data/lib/mailkick.rb CHANGED
@@ -1,19 +1,31 @@
1
- require "mailkick/version"
2
- require "mailkick/engine"
3
- require "mailkick/processor"
4
- require "mailkick/mailer"
1
+ # dependencies
2
+ require "active_support"
3
+
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"
13
+ require "mailkick/service/mailgun"
8
14
  require "mailkick/service/mandrill"
9
15
  require "mailkick/service/sendgrid"
10
- require "mailkick/service/mailgun"
11
- require "set"
16
+ require "mailkick/service/sendgrid_v2"
17
+ require "mailkick/service/postmark"
18
+ require "mailkick/url_helper"
19
+ require "mailkick/version"
20
+
21
+ # integrations
22
+ require "mailkick/engine" if defined?(Rails)
12
23
 
13
24
  module Mailkick
14
- mattr_accessor :services, :user_method, :secret_token
25
+ mattr_accessor :services, :secret_token, :mount, :process_opt_outs_method
15
26
  self.services = []
16
- self.user_method = proc { |email| User.where(email: email).first rescue nil }
27
+ self.mount = true
28
+ self.process_opt_outs_method = ->(_) { raise "process_opt_outs_method not defined" }
17
29
 
18
30
  def self.fetch_opt_outs
19
31
  services.each(&:fetch_opt_outs)
@@ -25,67 +37,22 @@ module Mailkick
25
37
  end
26
38
  end
27
39
 
28
- def self.opted_out?(options)
29
- opt_outs(options).any?
30
- end
31
-
32
- def self.opt_out(options)
33
- unless opted_out?(options)
34
- time = options[:time] || Time.now
35
- Mailkick::OptOut.create! do |o|
36
- o.email = options[:email]
37
- o.user = options[:user]
38
- o.reason = options[:reason] || "unsubscribe"
39
- o.list = options[:list]
40
- o.created_at = time
41
- o.updated_at = time
42
- end
43
- end
44
- true
45
- end
46
-
47
- def self.opt_in(options)
48
- opt_outs(options).each do |opt_out|
49
- opt_out.active = false
50
- opt_out.save!
51
- end
52
- true
40
+ def self.message_verifier
41
+ @message_verifier ||= ActiveSupport::MessageVerifier.new(Mailkick.secret_token)
53
42
  end
54
43
 
55
- def self.opt_outs(options = {})
56
- relation = Mailkick::OptOut.where(active: true)
57
-
58
- parts = []
59
- binds = []
60
- if (email = options[:email])
61
- parts << "email = ?"
62
- binds << email
63
- end
64
- if (user = options[:user])
65
- parts << "user_id = ? and user_type = ?"
66
- binds.concat [user.id, user.class.name]
67
- end
68
- relation = relation.where(parts.join(" OR "), *binds) if parts.any?
69
-
70
- relation =
71
- if options[:list]
72
- relation.where("list IS NULL OR list = ?", options[:list])
73
- else
74
- relation.where("list IS NULL")
75
- end
76
-
77
- relation
78
- end
44
+ def self.generate_token(subscriber, list)
45
+ raise ArgumentError, "Missing subscriber" unless subscriber
46
+ raise ArgumentError, "Missing list" unless list.present?
79
47
 
80
- def self.opted_out_emails(options = {})
81
- Set.new(opt_outs(options).where("email IS NOT NULL").uniq.pluck(:email))
48
+ message_verifier.generate([nil, subscriber.id, subscriber.class.name, list])
82
49
  end
50
+ end
83
51
 
84
- # does not take into account emails
85
- def self.opted_out_users(options = {})
86
- Set.new(opt_outs(options).where("user_id IS NOT NULL").map(&:user))
87
- end
52
+ ActiveSupport.on_load :action_mailer do
53
+ helper Mailkick::UrlHelper
88
54
  end
89
55
 
90
- ActionMailer::Base.send(:prepend, Mailkick::Mailer)
91
- ActiveRecord::Base.send(:extend, Mailkick::Model) if defined?(ActiveRecord)
56
+ ActiveSupport.on_load(:active_record) do
57
+ extend Mailkick::Model
58
+ end
metadata CHANGED
@@ -1,81 +1,65 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-21 00:00:00.000000000 Z
11
+ date: 2021-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.6'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.6'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
14
+ name: activesupport
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
19
+ version: '5.2'
20
+ type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
- version: '0'
41
- description: Email subscriptions made easy
42
- email:
43
- - andrew@chartkick.com
26
+ version: '5.2'
27
+ description:
28
+ email: andrew@ankane.org
44
29
  executables: []
45
30
  extensions: []
46
31
  extra_rdoc_files: []
47
32
  files:
48
- - ".gitignore"
49
33
  - CHANGELOG.md
50
- - Gemfile
51
34
  - LICENSE.txt
52
35
  - README.md
53
- - Rakefile
54
36
  - app/controllers/mailkick/subscriptions_controller.rb
55
- - app/helpers/mailkick/url_helper.rb
56
37
  - app/models/mailkick/opt_out.rb
38
+ - app/models/mailkick/subscription.rb
57
39
  - app/views/mailkick/subscriptions/show.html.erb
58
40
  - config/routes.rb
59
41
  - lib/generators/mailkick/install_generator.rb
60
- - lib/generators/mailkick/templates/install.rb
42
+ - lib/generators/mailkick/templates/install.rb.tt
61
43
  - lib/generators/mailkick/views_generator.rb
62
44
  - lib/mailkick.rb
63
45
  - lib/mailkick/engine.rb
64
- - lib/mailkick/mailer.rb
46
+ - lib/mailkick/legacy.rb
65
47
  - lib/mailkick/model.rb
66
- - lib/mailkick/processor.rb
67
48
  - lib/mailkick/service.rb
49
+ - lib/mailkick/service/aws_ses.rb
68
50
  - lib/mailkick/service/mailchimp.rb
69
51
  - lib/mailkick/service/mailgun.rb
70
52
  - lib/mailkick/service/mandrill.rb
53
+ - lib/mailkick/service/postmark.rb
71
54
  - lib/mailkick/service/sendgrid.rb
55
+ - lib/mailkick/service/sendgrid_v2.rb
56
+ - lib/mailkick/url_helper.rb
72
57
  - lib/mailkick/version.rb
73
- - mailkick.gemspec
74
58
  homepage: https://github.com/ankane/mailkick
75
59
  licenses:
76
60
  - MIT
77
61
  metadata: {}
78
- post_install_message:
62
+ post_install_message:
79
63
  rdoc_options: []
80
64
  require_paths:
81
65
  - lib
@@ -83,17 +67,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
67
  requirements:
84
68
  - - ">="
85
69
  - !ruby/object:Gem::Version
86
- version: '0'
70
+ version: '2.6'
87
71
  required_rubygems_version: !ruby/object:Gem::Requirement
88
72
  requirements:
89
73
  - - ">="
90
74
  - !ruby/object:Gem::Version
91
75
  version: '0'
92
76
  requirements: []
93
- rubyforge_project:
94
- rubygems_version: 2.4.5.1
95
- signing_key:
77
+ rubygems_version: 3.2.3
78
+ signing_key:
96
79
  specification_version: 4
97
- summary: Email subscriptions made easy
80
+ summary: Email subscriptions for Rails
98
81
  test_files: []
99
- has_rdoc: