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 +4 -4
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +1 -1
- data/README.md +38 -8
- data/app/controllers/mailkick/subscriptions_controller.rb +16 -14
- data/app/views/mailkick/subscriptions/show.html.erb +16 -10
- data/config/routes.rb +2 -1
- data/lib/generators/mailkick/install_generator.rb +12 -0
- data/lib/generators/mailkick/templates/install.rb.tt +2 -2
- data/lib/generators/mailkick/views_generator.rb +1 -1
- data/lib/mailkick/engine.rb +2 -17
- data/lib/mailkick/model.rb +7 -5
- data/lib/mailkick/service/aws_ses.rb +1 -2
- data/lib/mailkick/service/mailchimp.rb +3 -3
- data/lib/mailkick/service/mailgun.rb +3 -3
- data/lib/mailkick/service/mandrill.rb +2 -4
- data/lib/mailkick/service/postmark.rb +12 -11
- data/lib/mailkick/service/sendgrid.rb +21 -14
- data/lib/mailkick/version.rb +1 -1
- data/lib/mailkick.rb +6 -6
- metadata +6 -10
- data/app/models/mailkick/opt_out.rb +0 -7
- data/lib/mailkick/legacy.rb +0 -70
- data/lib/mailkick/serializer.rb +0 -15
- data/lib/mailkick/service/sendgrid_v2.rb +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53b4cfd8b344ad6273e734bad7531baaab340b50777e2e0590f2780b8ba72e96
|
|
4
|
+
data.tar.gz: 99d9c057e6fba352bc509368d49e4abda33fc6abd996ffef199866943e8625bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
[](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
|
-
|
|
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 =
|
|
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["
|
|
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::
|
|
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
|
-
|
|
12
|
+
defer = Mailkick.confirm_unsubscribe && request.get? && !params[:confirmed]
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
70
|
-
if
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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><%=
|
|
31
|
+
<p><%= button_to "Resubscribe", subscribe_url %></p>
|
|
30
32
|
<% else %>
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
6
|
+
source_root File.expand_path("../../../app/views", __dir__)
|
|
7
7
|
|
|
8
8
|
def copy_initializer_file
|
|
9
9
|
directory "mailkick", "app/views/mailkick"
|
data/lib/mailkick/engine.rb
CHANGED
|
@@ -5,23 +5,8 @@ module Mailkick
|
|
|
5
5
|
initializer "mailkick" do |app|
|
|
6
6
|
Mailkick.discover_services unless Mailkick.services.any?
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
data/lib/mailkick/model.rb
CHANGED
|
@@ -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
|
|
8
|
+
scope "#{prefix}subscribed", ->(list) { joins(:mailkick_subscriptions).where(mailkick_subscriptions: {list: list}) }
|
|
7
9
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
+
define_method("#{prefix}subscribed?") do |list|
|
|
19
21
|
mailkick_subscriptions.where(list: list).exists?
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Mailkick
|
|
4
4
|
class Service
|
|
5
5
|
class Mailchimp < Mailkick::Service
|
|
6
|
-
def initialize(
|
|
7
|
-
@gibbon = ::Gibbon::Request.new(api_key:
|
|
8
|
-
@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(
|
|
6
|
+
def initialize(api_key: nil, domain: nil)
|
|
7
7
|
require "mailgun"
|
|
8
|
-
mailgun_client = ::Mailgun::Client.new(
|
|
9
|
-
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
|
-
|
|
14
|
-
def initialize(options = {})
|
|
13
|
+
def initialize(api_key: nil)
|
|
15
14
|
require "mandrill"
|
|
16
15
|
@mandrill = ::Mandrill::API.new(
|
|
17
|
-
|
|
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/
|
|
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
|
-
"
|
|
7
|
+
"HardBounce" => "bounce",
|
|
8
8
|
"SpamComplaint" => "spam",
|
|
9
|
-
"
|
|
9
|
+
"ManualSuppression" => "unsubscribe"
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
def initialize(
|
|
13
|
-
@client = ::Postmark::ApiClient.new(
|
|
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
|
-
|
|
18
|
+
suppressions
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
def
|
|
21
|
-
fetch(@client.
|
|
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[:
|
|
34
|
-
time: ActiveSupport::TimeZone["UTC"].parse(record[:
|
|
35
|
-
reason: REASONS_MAP
|
|
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/
|
|
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(
|
|
7
|
-
@
|
|
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(
|
|
15
|
+
fetch(client.suppression.unsubscribes, "unsubscribe")
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def spam_reports
|
|
21
|
-
fetch(
|
|
19
|
+
fetch(client.suppression.spam_reports, "spam")
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
def bounces
|
|
25
|
-
fetch(
|
|
23
|
+
fetch(client.suppression.bounces, "bounce")
|
|
26
24
|
end
|
|
27
25
|
|
|
28
26
|
def self.discoverable?
|
|
29
|
-
!!(defined?(::
|
|
27
|
+
!!(defined?(::SendGrid::API) && ENV["SENDGRID_API_KEY"])
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
protected
|
|
33
31
|
|
|
34
|
-
def
|
|
35
|
-
|
|
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[
|
|
38
|
-
time: record[
|
|
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
|
-
|
|
53
|
+
SendGridV2 = SendGrid
|
|
47
54
|
end
|
|
48
55
|
end
|
data/lib/mailkick/version.rb
CHANGED
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
71
|
+
rubygems_version: 4.0.16
|
|
76
72
|
specification_version: 4
|
|
77
73
|
summary: Email subscriptions for Rails
|
|
78
74
|
test_files: []
|
data/lib/mailkick/legacy.rb
DELETED
|
@@ -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
|
data/lib/mailkick/serializer.rb
DELETED
|
@@ -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
|