mailkick 1.4.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f16bb5b552006639122c94a4b910982ed0845694bb503dfac08e23e3ed41f24
4
- data.tar.gz: c82b3290c80af68f5d9817a38a4d30af8d5893a216121154182457d96f0904dc
3
+ metadata.gz: 53b4cfd8b344ad6273e734bad7531baaab340b50777e2e0590f2780b8ba72e96
4
+ data.tar.gz: 99d9c057e6fba352bc509368d49e4abda33fc6abd996ffef199866943e8625bf
5
5
  SHA512:
6
- metadata.gz: a31f0bb8e3a84e0abcc6bca052a815fd4f98eb8130572903e010d479bc3e46c219ea0efb4b89cfba09d8cb3e7fd68a42f7c99716a1e4228ea9b65af6e663b21e
7
- data.tar.gz: e9e0cc2fac1d027ee28650077977d30abcce514e4064eb27b73e0650147828f292bcae32631cdb600271b9a5e50ddd24603e1d328231a1227d523571cfdb003c
6
+ metadata.gz: 31c98f092a8fcf4f2deef3b6c2a1d385e1b7cc13df3d05ddaa35b9cd460c9d9fca966972abc59ba5cf20d3ff2442410ad809d109af1ddbb9c178ee14bb934701
7
+ data.tar.gz: 3b6b152bd2154d13fb88f5fe38d67297a4909c3b7f95b8401f7065020501f59eff2bad0642cd38be980f07df8b26506376c669d87c580886b765b521b59419bd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 3.0.0 (2026-08-08)
2
+
3
+ See the [upgrade notes](https://github.com/ankane/mailkick?tab=readme-ov-file#30)
4
+
5
+ - Added `confirm_unsubscribe` option
6
+ - Enabled `headers` by default
7
+ - Improved CSP support
8
+ - Fixed change to key generator hash digest class
9
+ - Dropped support for Ruby < 3.3 and Rails < 7.2
10
+
11
+ ## 2.0.0 (2025-06-23)
12
+
13
+ See the [upgrade notes](https://github.com/ankane/mailkick/blob/v2.0.0/README.md#20)
14
+
15
+ - Added `prefix` option to `has_subscriptions`
16
+ - Switched to suppressions API for Postmark and changed default stream to `broadcast`
17
+ - Dropped support for legacy opt-outs
18
+ - Dropped support for legacy secret token generation and serialization
19
+ - Dropped support for legacy SendGrid service
20
+
1
21
  ## 1.4.0 (2025-04-03)
2
22
 
3
23
  - Dropped support for Ruby < 3.2 and Rails < 7.1
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2025 Andrew Kane
1
+ Copyright (c) 2014-2026 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -5,6 +5,8 @@ Email subscriptions for Rails
5
5
  - Add one-click unsubscribe links and headers to your emails
6
6
  - Fetch bounces and spam reports from your email service
7
7
 
8
+ **Mailkick 3.0 was recently released** - see [how to upgrade](#upgrading)
9
+
8
10
  :postbox: Check out [Ahoy Email](https://github.com/ankane/ahoy_email) for analytics
9
11
 
10
12
  [![Build Status](https://github.com/ankane/mailkick/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/mailkick/actions)
@@ -81,16 +83,20 @@ rails generate mailkick:views
81
83
 
82
84
  which copies the view into `app/views/mailkick`.
83
85
 
86
+ To add a confirmation step to prevent issues with scanners, create `config/initializers/mailkick.rb` with:
87
+
88
+ ```ruby
89
+ Mailkick.confirm_unsubscribe = true
90
+ ```
91
+
84
92
  ## Unsubscribe Headers
85
93
 
86
- For one-click unsubscribe headers ([RFC 8058](https://datatracker.ietf.org/doc/html/rfc8058)), create `config/initializers/mailkick.rb` with:
94
+ One-click unsubscribe headers ([RFC 8058](https://datatracker.ietf.org/doc/html/rfc8058)) are automatically added to emails that call `mailkick_unsubscribe_url`. To disable, create `config/initializers/mailkick.rb` with:
87
95
 
88
96
  ```ruby
89
- Mailkick.headers = true
97
+ Mailkick.headers = false
90
98
  ```
91
99
 
92
- Headers will automatically be added to emails that call `mailkick_unsubscribe_url`.
93
-
94
100
  ## Bounces and Spam Reports
95
101
 
96
102
  Fetch bounces, spam reports, and unsubscribes from your email service. Create `config/initializers/mailkick.rb` with a method to handle opt outs.
@@ -126,8 +132,6 @@ The following services are supported:
126
132
  - [Postmark](#postmark)
127
133
  - [SendGrid](#sendgrid)
128
134
 
129
- Will gladly accept pull requests for others.
130
-
131
135
  #### AWS SES
132
136
 
133
137
  Add the gem
@@ -168,7 +172,7 @@ Add the gem
168
172
  gem "mandrill-api"
169
173
  ```
170
174
 
171
- And set `ENV["MANDRILL_APIKEY"]`.
175
+ And set `ENV["MANDRILL_API_KEY"]`.
172
176
 
173
177
  #### Postmark
174
178
 
@@ -196,7 +200,7 @@ For more control over services, set them by hand.
196
200
 
197
201
  ```ruby
198
202
  Mailkick.services = [
199
- Mailkick::Service::SendGridV2.new(api_key: "API_KEY"),
203
+ Mailkick::Service::SendGrid.new(api_key: "API_KEY"),
200
204
  Mailkick::Service::Mailchimp.new(api_key: "API_KEY", list_id: "LIST_ID")
201
205
  ]
202
206
  ```
@@ -209,6 +213,32 @@ Access the subscription model directly
209
213
  Mailkick::Subscription.all
210
214
  ```
211
215
 
216
+ Prefix method names with `mailkick_`
217
+
218
+ ```ruby
219
+ class User < ApplicationRecord
220
+ has_subscriptions prefix: true
221
+ end
222
+ ```
223
+
224
+ ## Upgrading
225
+
226
+ ### 3.0
227
+
228
+ Versions 1.1.0-2.0.0 cause applications to use SHA1 for the key generator hash digest class if `Mailkick.secret_token` is not manually set (due to [this behavior in Rails](https://github.com/rails/rails/issues/56736)).
229
+
230
+ To avoid breakage when upgrading, add to `config/application.rb`:
231
+
232
+ ```ruby
233
+ config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA1
234
+ ```
235
+
236
+ Also, one-click unsubscribe headers are enabled by default. To disable, create an initializer with:
237
+
238
+ ```ruby
239
+ Mailkick.headers = false
240
+ ```
241
+
212
242
  ## History
213
243
 
214
244
  View the [changelog](https://github.com/ankane/mailkick/blob/master/CHANGELOG.md)
@@ -9,14 +9,16 @@ module Mailkick
9
9
  end
10
10
 
11
11
  def unsubscribe
12
- subscription.delete_all
12
+ defer = Mailkick.confirm_unsubscribe && request.get? && !params[:confirmed]
13
13
 
14
- Mailkick::Legacy.opt_out(legacy_options) if Mailkick::Legacy.opt_outs?
14
+ subscription.delete_all unless defer
15
15
 
16
16
  if request.post? && params["List-Unsubscribe"] == "One-Click"
17
17
  # must not redirect according to RFC 8058
18
18
  # could render show action instead
19
19
  render plain: "Unsubscribe successful"
20
+ elsif defer
21
+ render :show
20
22
  else
21
23
  redirect_to subscription_path(params[:id])
22
24
  end
@@ -25,8 +27,6 @@ module Mailkick
25
27
  def subscribe
26
28
  subscription.first_or_create!
27
29
 
28
- Mailkick::Legacy.opt_in(legacy_options) if Mailkick::Legacy.opt_outs?
29
-
30
30
  redirect_to subscription_path(params[:id])
31
31
  end
32
32
 
@@ -56,26 +56,28 @@ module Mailkick
56
56
  end
57
57
  helper_method :opted_out?
58
58
 
59
+ def unsubscribing?
60
+ action_name == "unsubscribe"
61
+ end
62
+ helper_method :unsubscribing?
63
+
59
64
  def subscribe_url
60
65
  subscribe_subscription_path(params[:id])
61
66
  end
62
67
  helper_method :subscribe_url
63
68
 
69
+ # custom views created before 3.0 will use GET instead of POST
70
+ # add confirmed parameter to ensure unsubscribed
64
71
  def unsubscribe_url
65
- unsubscribe_subscription_path(params[:id])
72
+ unsubscribe_subscription_path(params[:id], {confirmed: true})
66
73
  end
67
74
  helper_method :unsubscribe_url
68
75
 
69
- def legacy_options
70
- if @subscriber_type
71
- # on the unprobabilistic chance subscriber_type is compromised, not much damage
72
- user = @subscriber_type.constantize.find(@subscriber_id)
76
+ def style_nonce
77
+ if request.content_security_policy_nonce_directives&.include?("style-src")
78
+ content_security_policy_nonce
73
79
  end
74
- {
75
- email: @email,
76
- user: user,
77
- list: @list
78
- }
79
80
  end
81
+ helper_method :style_nonce
80
82
  end
81
83
  end
@@ -3,33 +3,39 @@
3
3
  <head>
4
4
  <title>Subscriptions</title>
5
5
 
6
- <style>
6
+ <%= content_tag :style, nonce: style_nonce do %>
7
7
  body {
8
8
  padding: 20px;
9
- font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
9
+ font-family: system-ui, "Helvetica Neue", Arial, Helvetica, sans-serif;
10
10
  font-size: 14px;
11
11
  line-height: 1.4;
12
12
  }
13
13
 
14
- a {
15
- color: blue;
16
- }
17
-
18
14
  .container {
19
15
  max-width: 500px;
20
16
  margin-left: auto;
21
17
  margin-right: auto;
22
18
  }
23
- </style>
19
+
20
+ button {
21
+ cursor: pointer;
22
+ font: inherit;
23
+ padding: 5px 10px;
24
+ }
25
+ <% end %>
24
26
  </head>
25
27
  <body>
26
28
  <div class="container">
27
29
  <% if !subscribed? %>
28
30
  <p>You are unsubscribed.</p>
29
- <p><%= link_to "Resubscribe", subscribe_url %></p>
31
+ <p><%= button_to "Resubscribe", subscribe_url %></p>
30
32
  <% else %>
31
- <p>You are subscribed.</p>
32
- <p><%= link_to "Unsubscribe", unsubscribe_url %></p>
33
+ <% if unsubscribing? %>
34
+ <p>Click the button below to confirm.</p>
35
+ <% else %>
36
+ <p>You are subscribed.</p>
37
+ <% end %>
38
+ <p><%= button_to "Unsubscribe", unsubscribe_url %></p>
33
39
  <% end %>
34
40
  </div>
35
41
  </body>
data/config/routes.rb CHANGED
@@ -7,6 +7,7 @@ end
7
7
  Mailkick::Engine.routes.draw do
8
8
  resources :subscriptions, only: [:show] do
9
9
  match :unsubscribe, on: :member, via: [:get, :post]
10
- get :subscribe, on: :member
10
+ # support GET for custom views created before 3.0
11
+ match :subscribe, on: :member, via: [:get, :post]
11
12
  end
12
13
  end
@@ -14,6 +14,18 @@ module Mailkick
14
14
  def migration_version
15
15
  "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
16
16
  end
17
+
18
+ def primary_key_type
19
+ ", id: :#{key_type}" if key_type
20
+ end
21
+
22
+ def foreign_key_type
23
+ ", type: :#{key_type}" if key_type
24
+ end
25
+
26
+ def key_type
27
+ Rails.configuration.generators.options.dig(:active_record, :primary_key_type)
28
+ end
17
29
  end
18
30
  end
19
31
  end
@@ -1,7 +1,7 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :mailkick_subscriptions do |t|
4
- t.references :subscriber, polymorphic: true, index: false
3
+ create_table :mailkick_subscriptions<%= primary_key_type %> do |t|
4
+ t.references :subscriber<%= foreign_key_type %>, polymorphic: true, index: false
5
5
  t.string :list
6
6
  t.timestamps
7
7
  end
@@ -3,7 +3,7 @@ require "rails/generators"
3
3
  module Mailkick
4
4
  module Generators
5
5
  class ViewsGenerator < Rails::Generators::Base
6
- source_root File.expand_path("../../../../app/views", __FILE__)
6
+ source_root File.expand_path("../../../app/views", __dir__)
7
7
 
8
8
  def copy_initializer_file
9
9
  directory "mailkick", "app/views/mailkick"
@@ -5,23 +5,8 @@ module Mailkick
5
5
  initializer "mailkick" do |app|
6
6
  Mailkick.discover_services unless Mailkick.services.any?
7
7
 
8
- unless Mailkick.secret_token
9
- Mailkick.secret_token = app.key_generator.generate_key("mailkick")
10
- Mailkick.message_verifier = ActiveSupport::MessageVerifier.new(Mailkick.secret_token, serializer: JSON)
11
-
12
- # TODO remove in 2.0
13
- creds =
14
- if app.respond_to?(:credentials) && app.credentials.secret_key_base
15
- app.credentials
16
- elsif app.respond_to?(:secrets) && app.config.paths["config/secrets"].existent.any?
17
- app.secrets
18
- else
19
- app.config
20
- end
21
-
22
- token = creds.respond_to?(:secret_key_base) ? creds.secret_key_base : creds.secret_token
23
- Mailkick.message_verifier.rotate(token, serializer: Marshal) if token
24
- end
8
+ # avoid app.key_generator due to https://github.com/ankane/ahoy_email/pull/168
9
+ Mailkick.secret_token ||= ActiveSupport::KeyGenerator.new(app.secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1).generate_key("mailkick")
25
10
  end
26
11
  end
27
12
  end
@@ -1,21 +1,23 @@
1
1
  module Mailkick
2
2
  module Model
3
- def has_subscriptions
3
+ def has_subscriptions(prefix: false)
4
+ prefix = prefix ? "mailkick_" : ""
5
+
4
6
  class_eval do
5
7
  has_many :mailkick_subscriptions, class_name: "Mailkick::Subscription", as: :subscriber
6
- scope :subscribed, ->(list) { joins(:mailkick_subscriptions).where(mailkick_subscriptions: {list: list}) }
8
+ scope "#{prefix}subscribed", ->(list) { joins(:mailkick_subscriptions).where(mailkick_subscriptions: {list: list}) }
7
9
 
8
- def subscribe(list)
10
+ define_method("#{prefix}subscribe") do |list|
9
11
  mailkick_subscriptions.where(list: list).first_or_create!
10
12
  nil
11
13
  end
12
14
 
13
- def unsubscribe(list)
15
+ define_method("#{prefix}unsubscribe") do |list|
14
16
  mailkick_subscriptions.where(list: list).delete_all
15
17
  nil
16
18
  end
17
19
 
18
- def subscribed?(list)
20
+ define_method("#{prefix}subscribed?") do |list|
19
21
  mailkick_subscriptions.where(list: list).exists?
20
22
  end
21
23
  end
@@ -8,8 +8,7 @@ module Mailkick
8
8
  "COMPLAINT" => "spam"
9
9
  }
10
10
 
11
- def initialize(options = {})
12
- @options = options
11
+ def initialize
13
12
  end
14
13
 
15
14
  def opt_outs
@@ -3,9 +3,9 @@
3
3
  module Mailkick
4
4
  class Service
5
5
  class Mailchimp < Mailkick::Service
6
- def initialize(options = {})
7
- @gibbon = ::Gibbon::Request.new(api_key: options[:api_key] || ENV["MAILCHIMP_API_KEY"])
8
- @list_id = options[:list_id] || ENV["MAILCHIMP_LIST_ID"]
6
+ def initialize(api_key: nil, list_id: nil)
7
+ @gibbon = ::Gibbon::Request.new(api_key: api_key || ENV["MAILCHIMP_API_KEY"])
8
+ @list_id = list_id || ENV["MAILCHIMP_LIST_ID"]
9
9
  end
10
10
 
11
11
  # TODO paginate
@@ -3,10 +3,10 @@
3
3
  module Mailkick
4
4
  class Service
5
5
  class Mailgun < Mailkick::Service
6
- def initialize(options = {})
6
+ def initialize(api_key: nil, domain: nil)
7
7
  require "mailgun"
8
- mailgun_client = ::Mailgun::Client.new(options[:api_key] || ENV["MAILGUN_API_KEY"])
9
- domain = options[:domain] || ActionMailer::Base.smtp_settings[:domain]
8
+ mailgun_client = ::Mailgun::Client.new(api_key || ENV["MAILGUN_API_KEY"])
9
+ domain ||= ActionMailer::Base.smtp_settings[:domain]
10
10
  @mailgun_events = ::Mailgun::Events.new(mailgun_client, domain)
11
11
  end
12
12
 
@@ -10,11 +10,10 @@ module Mailkick
10
10
  "unsub" => "unsubscribe"
11
11
  }
12
12
 
13
- # TODO remove ENV["MANDRILL_APIKEY"]
14
- def initialize(options = {})
13
+ def initialize(api_key: nil)
15
14
  require "mandrill"
16
15
  @mandrill = ::Mandrill::API.new(
17
- options[:api_key] || ENV["MANDRILL_APIKEY"] || ENV["MANDRILL_API_KEY"]
16
+ api_key || ENV["MANDRILL_APIKEY"] || ENV["MANDRILL_API_KEY"]
18
17
  )
19
18
  end
20
19
 
@@ -29,7 +28,6 @@ module Mailkick
29
28
  end
30
29
  end
31
30
 
32
- # TODO remove ENV["MANDRILL_APIKEY"]
33
31
  def self.discoverable?
34
32
  !!(defined?(::Mandrill::API) && (ENV["MANDRILL_APIKEY"] || ENV["MANDRILL_API_KEY"]))
35
33
  end
@@ -1,24 +1,25 @@
1
- # https://github.com/wildbit/postmark-gem
1
+ # https://github.com/ActiveCampaign/postmark-gem
2
2
 
3
3
  module Mailkick
4
4
  class Service
5
5
  class Postmark < Mailkick::Service
6
6
  REASONS_MAP = {
7
- "SpamNotification" => "spam",
7
+ "HardBounce" => "bounce",
8
8
  "SpamComplaint" => "spam",
9
- "Unsubscribe" => "unsubscribe"
9
+ "ManualSuppression" => "unsubscribe"
10
10
  }
11
11
 
12
- def initialize(options = {})
13
- @client = ::Postmark::ApiClient.new(options[:api_key] || ENV["POSTMARK_API_KEY"])
12
+ def initialize(api_key: nil, stream_id: "broadcast")
13
+ @client = ::Postmark::ApiClient.new(api_key || ENV["POSTMARK_API_KEY"])
14
+ @stream_id = stream_id
14
15
  end
15
16
 
16
17
  def opt_outs
17
- bounces
18
+ suppressions
18
19
  end
19
20
 
20
- def bounces
21
- fetch(@client.bounces)
21
+ def suppressions
22
+ fetch(@client.dump_suppressions(@stream_id))
22
23
  end
23
24
 
24
25
  def self.discoverable?
@@ -30,9 +31,9 @@ module Mailkick
30
31
  def fetch(response)
31
32
  response.map do |record|
32
33
  {
33
- email: record[:email],
34
- time: ActiveSupport::TimeZone["UTC"].parse(record[:bounced_at]),
35
- reason: REASONS_MAP.fetch(record[:type], "bounce")
34
+ email: record[:email_address],
35
+ time: ActiveSupport::TimeZone["UTC"].parse(record[:created_at]),
36
+ reason: REASONS_MAP[record[:suppression_reason]]
36
37
  }
37
38
  end
38
39
  end
@@ -1,41 +1,48 @@
1
- # https://github.com/freerobby/sendgrid_toolkit
1
+ # https://github.com/sendgrid/sendgrid-ruby
2
2
 
3
3
  module Mailkick
4
4
  class Service
5
5
  class SendGrid < Mailkick::Service
6
- def initialize(options = {})
7
- @api_user = options[:api_user] || ENV["SENDGRID_USERNAME"]
8
- @api_key = options[:api_key] || ENV["SENDGRID_PASSWORD"]
6
+ def initialize(api_key: nil)
7
+ @api_key = api_key || ENV["SENDGRID_API_KEY"]
9
8
  end
10
9
 
11
- # TODO paginate
12
10
  def opt_outs
13
11
  unsubscribes + spam_reports + bounces
14
12
  end
15
13
 
16
14
  def unsubscribes
17
- fetch(::SendgridToolkit::Unsubscribes, "unsubscribe")
15
+ fetch(client.suppression.unsubscribes, "unsubscribe")
18
16
  end
19
17
 
20
18
  def spam_reports
21
- fetch(::SendgridToolkit::SpamReports, "spam")
19
+ fetch(client.suppression.spam_reports, "spam")
22
20
  end
23
21
 
24
22
  def bounces
25
- fetch(::SendgridToolkit::Bounces, "bounce")
23
+ fetch(client.suppression.bounces, "bounce")
26
24
  end
27
25
 
28
26
  def self.discoverable?
29
- !!(defined?(::SendgridToolkit) && ENV["SENDGRID_USERNAME"] && ENV["SENDGRID_PASSWORD"])
27
+ !!(defined?(::SendGrid::API) && ENV["SENDGRID_API_KEY"])
30
28
  end
31
29
 
32
30
  protected
33
31
 
34
- def fetch(klass, reason)
35
- klass.new(@api_user, @api_key).retrieve_with_timestamps.map do |record|
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|
36
43
  {
37
- email: record["email"],
38
- time: record["created"],
44
+ email: record[:email],
45
+ time: Time.at(record[:created]),
39
46
  reason: reason
40
47
  }
41
48
  end
@@ -43,6 +50,6 @@ module Mailkick
43
50
  end
44
51
 
45
52
  # backwards compatibility
46
- Sendgrid = SendGrid
53
+ SendGridV2 = SendGrid
47
54
  end
48
55
  end
@@ -1,3 +1,3 @@
1
1
  module Mailkick
2
- VERSION = "1.4.0"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/mailkick.rb CHANGED
@@ -6,16 +6,13 @@ require "json"
6
6
  require "set"
7
7
 
8
8
  # modules
9
- require_relative "mailkick/legacy"
10
9
  require_relative "mailkick/model"
11
- require_relative "mailkick/serializer"
12
10
  require_relative "mailkick/service"
13
11
  require_relative "mailkick/service/aws_ses"
14
12
  require_relative "mailkick/service/mailchimp"
15
13
  require_relative "mailkick/service/mailgun"
16
14
  require_relative "mailkick/service/mandrill"
17
15
  require_relative "mailkick/service/sendgrid"
18
- require_relative "mailkick/service/sendgrid_v2"
19
16
  require_relative "mailkick/service/postmark"
20
17
  require_relative "mailkick/url_helper"
21
18
  require_relative "mailkick/version"
@@ -24,22 +21,25 @@ require_relative "mailkick/version"
24
21
  require_relative "mailkick/engine" if defined?(Rails)
25
22
 
26
23
  module Mailkick
27
- mattr_accessor :services, :mount, :process_opt_outs_method, :headers
24
+ mattr_accessor :services, :mount, :process_opt_outs_method, :headers, :confirm_unsubscribe
28
25
  mattr_reader :secret_token
29
26
  mattr_writer :message_verifier
30
27
  self.services = []
31
28
  self.mount = true
32
29
  self.process_opt_outs_method = ->(_) { raise "process_opt_outs_method not defined" }
33
- self.headers = false
30
+ self.headers = true
31
+ self.confirm_unsubscribe = false
34
32
 
35
33
  def self.fetch_opt_outs
36
34
  services.each(&:fetch_opt_outs)
35
+ nil
37
36
  end
38
37
 
39
38
  def self.discover_services
40
39
  Service.subclasses.each do |service|
41
40
  services << service.new if service.discoverable?
42
41
  end
42
+ nil
43
43
  end
44
44
 
45
45
  def self.secret_token=(token)
@@ -48,7 +48,7 @@ module Mailkick
48
48
  end
49
49
 
50
50
  def self.message_verifier
51
- @@message_verifier ||= ActiveSupport::MessageVerifier.new(Mailkick.secret_token, serializer: Serializer)
51
+ @@message_verifier ||= ActiveSupport::MessageVerifier.new(Mailkick.secret_token, serializer: JSON)
52
52
  end
53
53
 
54
54
  def self.generate_token(subscriber, list)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '7.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.1'
25
+ version: '7.2'
26
26
  email: andrew@ankane.org
27
27
  executables: []
28
28
  extensions: []
@@ -32,7 +32,6 @@ files:
32
32
  - LICENSE.txt
33
33
  - README.md
34
34
  - app/controllers/mailkick/subscriptions_controller.rb
35
- - app/models/mailkick/opt_out.rb
36
35
  - app/models/mailkick/subscription.rb
37
36
  - app/views/mailkick/subscriptions/show.html.erb
38
37
  - config/routes.rb
@@ -41,9 +40,7 @@ files:
41
40
  - lib/generators/mailkick/views_generator.rb
42
41
  - lib/mailkick.rb
43
42
  - lib/mailkick/engine.rb
44
- - lib/mailkick/legacy.rb
45
43
  - lib/mailkick/model.rb
46
- - lib/mailkick/serializer.rb
47
44
  - lib/mailkick/service.rb
48
45
  - lib/mailkick/service/aws_ses.rb
49
46
  - lib/mailkick/service/mailchimp.rb
@@ -51,7 +48,6 @@ files:
51
48
  - lib/mailkick/service/mandrill.rb
52
49
  - lib/mailkick/service/postmark.rb
53
50
  - lib/mailkick/service/sendgrid.rb
54
- - lib/mailkick/service/sendgrid_v2.rb
55
51
  - lib/mailkick/url_helper.rb
56
52
  - lib/mailkick/version.rb
57
53
  homepage: https://github.com/ankane/mailkick
@@ -65,14 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
61
  requirements:
66
62
  - - ">="
67
63
  - !ruby/object:Gem::Version
68
- version: '3.2'
64
+ version: '3.3'
69
65
  required_rubygems_version: !ruby/object:Gem::Requirement
70
66
  requirements:
71
67
  - - ">="
72
68
  - !ruby/object:Gem::Version
73
69
  version: '0'
74
70
  requirements: []
75
- rubygems_version: 3.6.2
71
+ rubygems_version: 4.0.16
76
72
  specification_version: 4
77
73
  summary: Email subscriptions for Rails
78
74
  test_files: []
@@ -1,7 +0,0 @@
1
- module Mailkick
2
- class OptOut < ActiveRecord::Base
3
- self.table_name = "mailkick_opt_outs"
4
-
5
- belongs_to :user, polymorphic: true, optional: true
6
- end
7
- end
@@ -1,70 +0,0 @@
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_pool.with_connection { |c| c.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,15 +0,0 @@
1
- # need custom class due to
2
- # https://github.com/rails/rails/issues/47185
3
- module Mailkick
4
- class Serializer
5
- def self.dump(value)
6
- ActiveSupport::JSON.encode(value)
7
- end
8
-
9
- def self.load(value)
10
- ActiveSupport::JSON.decode(value)
11
- rescue JSON::ParserError
12
- Marshal.load(value)
13
- end
14
- end
15
- end
@@ -1,52 +0,0 @@
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