mailkick 0.1.4 → 1.0.1

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