mail_delivery_task 0.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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.rubocop.yml +29 -0
- data/CHANGES.md +3 -0
- data/Gemfile +22 -0
- data/LICENSE +13 -0
- data/README.md +283 -0
- data/Rakefile +14 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/generators/mail_delivery_task/install_generator.rb +37 -0
- data/lib/generators/mail_delivery_task/templates/create_mail_delivery_task_attempts.rb +39 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_batch_job.rb.erb +3 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_batch_job_spec.rb.erb +5 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_job.rb.erb +3 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_job_spec.rb.erb +5 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_task_attempt.rb.erb +24 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_task_attempt_spec.rb.erb +5 -0
- data/lib/generators/mail_delivery_task/templates/mail_delivery_task_attempts.rb.erb +27 -0
- data/lib/mail_delivery_task.rb +12 -0
- data/lib/mail_delivery_task/base_attempt.rb +125 -0
- data/lib/mail_delivery_task/jobs/base_delivery_batch_job.rb +18 -0
- data/lib/mail_delivery_task/jobs/base_delivery_job.rb +23 -0
- data/lib/mail_delivery_task/jobs/base_job.rb +16 -0
- data/lib/mail_delivery_task/testing.rb +1 -0
- data/lib/mail_delivery_task/testing/mailer_helper.rb +114 -0
- data/lib/mail_delivery_task/version.rb +3 -0
- data/mail_delivery_task.gemspec +28 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d058a7ce383210d1d6769a0e59a3e43e606086a
|
4
|
+
data.tar.gz: 0ba26ba17147024483910e2836b5645a67a61ea8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b071d4dbf1ee7de728d02513cfb74530b581cc7a00a6884c07a1ac6e7110dbb9d2a9c673d8152e8246705ee7d70142c010ece9a08cf03db42838a14db491294c
|
7
|
+
data.tar.gz: c09934358f9a0541ec55ad986e6e53bbf362a8379047e31fac7d8d6b3991844d5ce06a6be2edcb64d81090f617020baa67a07719757820601eefb68cf395f8b6
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/Gemfile.lock
|
4
|
+
/_yardoc/
|
5
|
+
/coverage/
|
6
|
+
/doc/
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/tmp/
|
10
|
+
|
11
|
+
# rspec failure tracking
|
12
|
+
.rspec_status
|
13
|
+
|
14
|
+
# Ignore all logfiles and tempfiles
|
15
|
+
*.log
|
16
|
+
*.swp
|
17
|
+
|
18
|
+
# Ignore all sqlite3 files
|
19
|
+
*.sqlite3
|
20
|
+
|
21
|
+
# rspec failure tracking
|
22
|
+
.rspec_status
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Lint/AmbiguousBlockAssociation:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Metrics/AbcSize:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Metrics/BlockLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/CyclomaticComplexity:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Metrics/MethodLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Metrics/LineLength:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Metrics/PerceivedComplexity:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Security/YAMLLoad:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Style/Documentation:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Style/FrozenStringLiteralComment:
|
29
|
+
Enabled: false
|
data/CHANGES.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in mail_delivery_task.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# Core
|
7
|
+
gem 'bundler', '~> 1.14'
|
8
|
+
gem 'rake', '~> 11.0'
|
9
|
+
|
10
|
+
# Development Experience
|
11
|
+
gem 'pry'
|
12
|
+
gem 'pry-byebug'
|
13
|
+
gem 'rubocop'
|
14
|
+
|
15
|
+
# Testing (see spec/dummy)
|
16
|
+
gem 'factory_bot'
|
17
|
+
gem 'rails', '~> 5.1.0'
|
18
|
+
gem 'rspec', '~> 3.5'
|
19
|
+
gem 'rspec-rails', '~> 3.0'
|
20
|
+
gem 'sqlite3'
|
21
|
+
|
22
|
+
gem 'timecop'
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2017 Square, Inc.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# MailDeliveryTask
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/mail_delivery_task)
|
4
|
+
[](https://github.com/square/mail_delivery_task/blob/master/LICENSE)
|
5
|
+
|
6
|
+
This gem provides generators and mixins to queue up mail delivery in database transactions to be
|
7
|
+
delivered later. Doing so prevents mail from being sent twice if the transaction is rolled back:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
transaction do
|
11
|
+
model = MyModel.create!(foo: 'hello')
|
12
|
+
MyMailer.mailer_action(model: model).deliver
|
13
|
+
raise
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
Despite database transaction rolling back the creation of the `MyModel` record, the mail is still
|
18
|
+
delivered. This problem becomes more difficult in nested transactions. To avoid this, we create a
|
19
|
+
a `MailDeliveryTask::Attempt` record inside the database. These records are then delivered at a
|
20
|
+
later time using a job:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
transaction do
|
24
|
+
model = MyModel.create!(foo: 'hello')
|
25
|
+
|
26
|
+
# To be sent by a job later
|
27
|
+
MailDeliveryTask::Attempt.create(
|
28
|
+
mailer_class: MyMailer,
|
29
|
+
mailer_action_name: :mailer_action,
|
30
|
+
mailer_args: { my_model: model },
|
31
|
+
idempotence_token: "my_model##{id}"
|
32
|
+
)
|
33
|
+
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
The above pattern ensures mail delivery tasks will not be created nor sent when the transaction
|
39
|
+
fails.
|
40
|
+
|
41
|
+
The gem provides the following:
|
42
|
+
|
43
|
+
* Models
|
44
|
+
* Generators for the `MailDeliveryTask::Attempt` migration, model, factory, and specs.
|
45
|
+
* Tracking completion using `completed_at`.
|
46
|
+
* Fields for `mailer_class_name`, `mailer_action_name`, and `mailer_args`.
|
47
|
+
* `MailDeliveryTask::BaseAttempt` mixin to provide model methods.
|
48
|
+
* Persistence token support.
|
49
|
+
* A `num_attempts` field gives you flexibility to handle retries and other failure scenarios.
|
50
|
+
* `status` and `completed_at` are fields that track state.
|
51
|
+
* Jobs
|
52
|
+
* Generators for `MailDeliveryTaskJob` and `MailDeliveryBatchJob` jobs and specs
|
53
|
+
* `MailDeliveryTask::BaseDeliveryJob` and `MailDeliveryTask::BaseDeliveryBatchJob` mixins.
|
54
|
+
|
55
|
+
## Design Motivations
|
56
|
+
|
57
|
+
We're relying heavily on generators and mixins. Including the `MailDeliveryTask::BaseAttempt` module
|
58
|
+
allows us to generate a model that can inherit from both `ActiveRecord::Base` (Rails 4) and
|
59
|
+
`ApplicationRecord` (Rails 5). The `BaseAttempt` module's methods can easily be overridden, giving
|
60
|
+
callers flexibility to handle errors, extend functionality, and inherit (STI). Lastly, the generated
|
61
|
+
migrations provide fields used by the `BaseAttempt` module, but the developer is free to add their
|
62
|
+
own fields and extend the module's methods while calling `super`.
|
63
|
+
|
64
|
+
This gem is also designed to be compatible with any `ApplicationMailer` implementation through the
|
65
|
+
use of the `mailer_class_name`, `mailer_action_name`, and `mailer_args` (keyword args) fields.
|
66
|
+
|
67
|
+
## Getting Started
|
68
|
+
|
69
|
+
1. Add the gem to your application's Gemfile and execute `bundle install` to install it:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
gem 'mail_delivery_task'
|
73
|
+
```
|
74
|
+
|
75
|
+
2. Generate migrations, base models, jobs, and specs. Feel free to add any additional columns you
|
76
|
+
may need to the generated migration file:
|
77
|
+
|
78
|
+
`$ rails generate mail_delivery_task:install`
|
79
|
+
|
80
|
+
3. You will need a working `ActionMailer` class to send mail through SMTP / Butter. **Note: the
|
81
|
+
mailer's arguments MUST be keyword arguments to be compatible with the `mailer_args` field in the
|
82
|
+
`MailDeliveryTask` model.**
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class DummyMailer < ApplicationMailer
|
86
|
+
# Keyword args required!!!
|
87
|
+
def action_name(to_address:)
|
88
|
+
mail(
|
89
|
+
to: to_address,
|
90
|
+
subject: 'How to setup mail_delivery_task',
|
91
|
+
body: "It's really easy.",
|
92
|
+
content_type: 'text/plain',
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
4. Rename the model and migrations as you see fit. Make sure your model contains
|
99
|
+
`include MailDeliveryTask::BaseAttempt`.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class MailDeliveryTask < ActiveRecord::Base
|
103
|
+
include MailDeliveryTask::BaseAttempt
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
5. Implement the `handle_deliver_mail_error` and `handle_persist_mail_error` in your `MailDeliveryTask`
|
108
|
+
model. These two methods are used by `MailDeliveryTask::BaseAttempt` when exceptions are thrown
|
109
|
+
delivering and persisting the mail. See cookbook below for details on persistence and error
|
110
|
+
handling.
|
111
|
+
|
112
|
+
6. Do not send mail directly using the `ActionMailer` class above. Instead, create
|
113
|
+
`MailDeliveryTask`s to be sent later by a job (generated) that includes a
|
114
|
+
`MailDeliveryTask::BaseDeliveryJob`:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class MailDeliveryJob < ActiveJob::Base
|
118
|
+
include MailDeliveryTask::BaseDeliveryJob
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
transaction do
|
124
|
+
# Using the DummyMailer class above...
|
125
|
+
MailDeliveryTask::Attempt.create(
|
126
|
+
mailer_class: DummyMailer,
|
127
|
+
mailer_action_name: :action_name,
|
128
|
+
mailer_args: { to_address: 'jchang@squareup.com' },
|
129
|
+
idempotence_token: 'token',
|
130
|
+
)
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
7. **Make sure to schedule the mail delivery job to run frequently using [`Clockwork`](https://github.com/adamwiggins/clockwork).**
|
135
|
+
|
136
|
+
8. If you would like to use something other than Trunk to persist mail, see the cookbook below on
|
137
|
+
using a different method of persistence.
|
138
|
+
|
139
|
+
## Improper Uses of the Gem
|
140
|
+
|
141
|
+
Below are patterns that defeat the purpose of using this gem:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
# DO NOT DO THIS
|
145
|
+
transaction do
|
146
|
+
task = create_mail_delivery_task
|
147
|
+
task.deliver!
|
148
|
+
raise
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
The above example allows mail to be delivered even if the transaction fails.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# DO NOT DO THIS
|
156
|
+
MailDeliverytask::Attempt.create(
|
157
|
+
mailer_class: 'DummyMailer',
|
158
|
+
mailer_action_name: 'action_name',
|
159
|
+
mailer_args: {},
|
160
|
+
).deliver!
|
161
|
+
```
|
162
|
+
|
163
|
+
These two examples above do not make use of a job to deliver mail.
|
164
|
+
|
165
|
+
## Cookbook
|
166
|
+
|
167
|
+
### Delayed Execution
|
168
|
+
|
169
|
+
Setting the `scheduled_at` field allows delayed execution to be possible. A task that has an
|
170
|
+
`scheduled_at` before `Time.current` will be executed by `MailDeliveryTask::BaseDeliveryBatchJob`.
|
171
|
+
|
172
|
+
### Overriding MailDeliveryTask::Base Error Handlers
|
173
|
+
|
174
|
+
By default, when persistence or deliverance fails, it just raises the error
|
175
|
+
encountered. However, if you want to raise a custom error or wrap the error,
|
176
|
+
you can override both of these by overriding the `handle_deliver_mail_error`
|
177
|
+
and `handle_persist_mail_error` methods.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class MailDeliveryTask::Attempt < ApplicationRecord
|
181
|
+
include MailDeliveryTask::BaseAttempt
|
182
|
+
|
183
|
+
class DeliverMailError < StandardError; end
|
184
|
+
class PersistMailError < StandardError; end
|
185
|
+
|
186
|
+
def handle_deliver_mail_error(error)
|
187
|
+
raise DeliverMailError, 'my custom error message'
|
188
|
+
end
|
189
|
+
|
190
|
+
def handle_persist_mail_error(error)
|
191
|
+
raise PersistMailError, 'my custom error message'
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
Lastly, the `num_attempts` field in `MailDeliveryTask::Attempt` allows you to track the number of
|
197
|
+
delivery attempts the mail has. Use this to implement retries and permanent failure thresholds for
|
198
|
+
your mail delivery tasks.
|
199
|
+
|
200
|
+
### Proper Usage of `expire!` / `fail!`
|
201
|
+
|
202
|
+
`expire!` should be used for mail that is no longer applicable, such as a mail for plan past due
|
203
|
+
when the plan is no longer past due.
|
204
|
+
|
205
|
+
`fail!` should be used to mark delivery as failed when the mail should have been, but was not,
|
206
|
+
delivered successfully.
|
207
|
+
|
208
|
+
### Persistence
|
209
|
+
|
210
|
+
If you wish to persist mail, override the `persist_mail` method:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
class MailDeliveryTask::Attempt < ApplicationRecord
|
214
|
+
include MailDeliveryTask::BaseAttempt
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def persist_mail(mail)
|
219
|
+
store_in_s3(mail.to_s)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
Don't forget to set the `persistence_token`.
|
225
|
+
|
226
|
+
### Custom Matchers for RSpec
|
227
|
+
|
228
|
+
Add the following lines to `rails_helper.rb`:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
require 'mail_delivery_task/testing'
|
232
|
+
|
233
|
+
RSpec.configure do |config|
|
234
|
+
config.include MailDeliveryTask::Testing::MailerHelper, type: :mailer
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
Now custom matchers like `be_deliverable` are enabled:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
expect(mail).to be_deliverable
|
242
|
+
```
|
243
|
+
|
244
|
+
For a full list of matchers, see [here](https://github.com/square/mail_delivery_task/tree/master/lib/mail_delivery_task/testing/mailer_helper.rb).
|
245
|
+
|
246
|
+
### Overriding the Mail Delivery Mechanism
|
247
|
+
|
248
|
+
Sometimes the mail delivery method found in the [BaseAttempt](https://github.com/square/mail_delivery_task/tree/master/lib/mail_delivery_task/base_attempt.rb) is insufficient. In this case you can override the method in your `MailDeliveryTask::Attempt`:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
class MailDeliveryTask::Attempt < ApplicationRecord
|
252
|
+
include MailDeliveryTask::BaseAttempt
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def deliver_mail(mail)
|
257
|
+
mail.deliver_some_other_way
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
## Development
|
263
|
+
|
264
|
+
* Install dependencies with `bin/setup`.
|
265
|
+
* Run tests/lints with `rake`
|
266
|
+
* For an interactive prompt that will allow you to experiment, run `bin/console`.
|
267
|
+
|
268
|
+
## License
|
269
|
+
|
270
|
+
```
|
271
|
+
Copyright 2017 Square, Inc.
|
272
|
+
|
273
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
274
|
+
you may not use this file except in compliance with the License.
|
275
|
+
You may obtain a copy of the License at
|
276
|
+
|
277
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
278
|
+
|
279
|
+
Unless required by applicable law or agreed to in writing, software
|
280
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
281
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
282
|
+
See the License for the specific language governing permissions and
|
283
|
+
limitations under the License.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'sq/gem_tasks'
|
4
|
+
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
|
8
|
+
require 'rubocop/rake_task'
|
9
|
+
RuboCop::RakeTask.new
|
10
|
+
|
11
|
+
require 'sq/lint/rake_task'
|
12
|
+
Sq::Lint::RakeTask.new
|
13
|
+
|
14
|
+
task default: %w(spec rubocop sq:lint:gem)
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "mail_delivery_task"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module MailDeliveryTask
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
include ::Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates/', __FILE__)
|
9
|
+
|
10
|
+
desc 'Generates (but does not run) migrations to add the' \
|
11
|
+
' mail_delivery_task_attempts table and creates the base model'
|
12
|
+
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_migration_file
|
18
|
+
migration_template 'create_mail_delivery_task_attempts.rb', 'db/migrate/create_mail_delivery_task_attempts.rb'
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_mail_delivery_task_files
|
22
|
+
template 'mail_delivery_task_attempt.rb.erb', 'app/models/mail_delivery_task/attempt.rb'
|
23
|
+
template 'mail_delivery_job.rb.erb', 'app/jobs/mail_delivery_job.rb'
|
24
|
+
template 'mail_delivery_batch_job.rb.erb', 'app/jobs/mail_delivery_batch_job.rb'
|
25
|
+
|
26
|
+
if defined?(RSpec)
|
27
|
+
template 'mail_delivery_task_attempt_spec.rb.erb', 'spec/models/mail_delivery_task/attempt_spec.rb'
|
28
|
+
template 'mail_delivery_job_spec.rb.erb', 'spec/jobs/mail_delivery_job_spec.rb'
|
29
|
+
template 'mail_delivery_batch_job_spec.rb.erb', 'spec/jobs/mail_delivery_batch_job_spec.rb'
|
30
|
+
end
|
31
|
+
|
32
|
+
if defined?(FactoryBot) || defined?(FactoryGirl)
|
33
|
+
template 'mail_delivery_task_attempts.rb.erb', 'spec/factories/mail_delivery_task/attempts.rb'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class CreateMailDeliveryTaskAttempts < ActiveRecord::Migration[5.1]
|
2
|
+
def change
|
3
|
+
create_table :mail_delivery_task_attempts do |t|
|
4
|
+
t.integer :lock_version, null: false, default: 0
|
5
|
+
|
6
|
+
t.string :status
|
7
|
+
t.string :idempotence_token, null: false
|
8
|
+
|
9
|
+
t.string :mailer_class_name, null: false
|
10
|
+
t.string :mailer_action_name, null: false
|
11
|
+
t.text :mailer_args
|
12
|
+
|
13
|
+
t.boolean :should_persist, default: false
|
14
|
+
t.string :mailer_message_id
|
15
|
+
t.string :persistence_token
|
16
|
+
|
17
|
+
t.integer :num_attempts, null: false, default: 0
|
18
|
+
|
19
|
+
t.datetime :scheduled_at
|
20
|
+
t.datetime :completed_at
|
21
|
+
|
22
|
+
t.timestamps null: false
|
23
|
+
|
24
|
+
t.index :status
|
25
|
+
t.index [:idempotence_token, :mailer_class_name, :mailer_action_name], unique: true, name: 'index_mdt_attempts_on_idempotence_token_and_mailer'
|
26
|
+
|
27
|
+
t.index [:mailer_class_name, :mailer_action_name], name: 'index_mdt_attempts_on_mailer_and_template'
|
28
|
+
|
29
|
+
t.index :should_persist
|
30
|
+
t.index :mailer_message_id
|
31
|
+
|
32
|
+
t.index :scheduled_at
|
33
|
+
t.index :completed_at
|
34
|
+
|
35
|
+
t.index :created_at
|
36
|
+
t.index :updated_at
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class MailDeliveryTask::Attempt < <% if Rails::VERSION::STRING >= '5' %>ApplicationRecord<% else %>ActiveRecord::Base<% end %>
|
2
|
+
include MailDeliveryTask::BaseAttempt
|
3
|
+
|
4
|
+
# @override
|
5
|
+
#
|
6
|
+
# Override this method to deliver mail.
|
7
|
+
def persist_mail(mail)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# @override
|
12
|
+
#
|
13
|
+
# This method is used by MailDeliveryTask::BaseAttempt when #perform! fails.
|
14
|
+
def handle_deliver_mail_error(error)
|
15
|
+
raise error
|
16
|
+
end
|
17
|
+
|
18
|
+
# @override
|
19
|
+
#
|
20
|
+
# This method is used by MailDeliveryTask::BaseAttempt when #perform! fails.
|
21
|
+
def handle_persist_mail_error(error)
|
22
|
+
raise error
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<% if defined?(FactoryBot) %>FactoryBot<% else %>FactoryGirl<%end%>.define do
|
2
|
+
factory :mail_delivery_task_attempt, class: 'MailDeliveryTask::Attempt' do
|
3
|
+
status { 'pending' }
|
4
|
+
|
5
|
+
idempotence_token { SecureRandom.hex }
|
6
|
+
|
7
|
+
mailer_class_name { 'DummyMailer' }
|
8
|
+
mailer_action_name { 'action_name' }
|
9
|
+
mailer_args { {} }
|
10
|
+
|
11
|
+
trait :delivered do
|
12
|
+
status { :delivered }
|
13
|
+
mailer_message_id { SecureRandom.hex(6) }
|
14
|
+
completed_at { Time.current }
|
15
|
+
end
|
16
|
+
|
17
|
+
trait :expired do
|
18
|
+
status { 'expired' }
|
19
|
+
completed_at { Time.current }
|
20
|
+
end
|
21
|
+
|
22
|
+
trait :failed do
|
23
|
+
status { 'failed' }
|
24
|
+
completed_at { Time.current }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'active_job'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'enumerize'
|
6
|
+
require 'with_advisory_lock'
|
7
|
+
|
8
|
+
require 'mail_delivery_task/version'
|
9
|
+
|
10
|
+
require 'mail_delivery_task/base_attempt'
|
11
|
+
|
12
|
+
Dir["#{File.dirname(__FILE__)}/mail_delivery_task/jobs/**/*.rb"].each { |file| require file }
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module MailDeliveryTask
|
2
|
+
class InvalidStateError < StandardError; end
|
3
|
+
|
4
|
+
module BaseAttempt
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
extend Enumerize
|
9
|
+
|
10
|
+
self.table_name = 'mail_delivery_task_attempts'
|
11
|
+
|
12
|
+
validates :mailer_class_name, presence: true
|
13
|
+
validates :mailer_action_name, presence: true
|
14
|
+
validates :mailer_message_id, uniqueness: true, allow_nil: true
|
15
|
+
|
16
|
+
serialize :mailer_args, JSON
|
17
|
+
|
18
|
+
scope :persisted, -> { where.not(persistence_token: nil) }
|
19
|
+
scope :pending, -> { where(status: :pending) }
|
20
|
+
scope :delivered, -> { where(status: :delivered) }
|
21
|
+
scope :expired, -> { where(status: :expired) }
|
22
|
+
scope :failed, -> { where(status: :failed) }
|
23
|
+
|
24
|
+
enumerize :status,
|
25
|
+
in: [:pending, :delivered, :expired, :failed],
|
26
|
+
predicates: true
|
27
|
+
|
28
|
+
before_create do
|
29
|
+
self.status ||= 'pending'
|
30
|
+
end
|
31
|
+
|
32
|
+
# For backward compatibility
|
33
|
+
alias_method :handle_deliver_error, :handle_deliver_mail_error
|
34
|
+
end
|
35
|
+
|
36
|
+
def deliver!
|
37
|
+
return unless may_schedule?
|
38
|
+
|
39
|
+
begin
|
40
|
+
reload
|
41
|
+
raise MailDeliveryTask::InvalidStateError unless pending?
|
42
|
+
increment!(:num_attempts)
|
43
|
+
rescue ActiveRecord::StaleObjectError
|
44
|
+
retry
|
45
|
+
end
|
46
|
+
|
47
|
+
mailer_class = mailer_class_name.constantize
|
48
|
+
|
49
|
+
mail = if mailer_args.present?
|
50
|
+
mailer_class.send(mailer_action_name, **mailer_args.symbolize_keys)
|
51
|
+
else
|
52
|
+
mailer_class.send(mailer_action_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
with_lock do
|
56
|
+
raise MailDeliveryTask::InvalidStateError unless pending?
|
57
|
+
|
58
|
+
begin
|
59
|
+
self.persistence_token = persist_mail(mail) if should_persist?
|
60
|
+
rescue StandardError => e
|
61
|
+
# If Trunk is down, simply catch the exception so that we won't retry
|
62
|
+
# and thus send the mail multiple times.
|
63
|
+
handle_persist_mail_error(e)
|
64
|
+
end
|
65
|
+
|
66
|
+
begin
|
67
|
+
# Perform the actual delivery through SMTP / Butter
|
68
|
+
deliver_mail(mail)
|
69
|
+
|
70
|
+
# Note that this fails silently.
|
71
|
+
self.mailer_message_id = mail.message_id
|
72
|
+
update_status!('delivered')
|
73
|
+
rescue StandardError => e
|
74
|
+
handle_deliver_error(e)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def expire!
|
80
|
+
with_lock do
|
81
|
+
raise MailDeliveryTask::InvalidStateError unless pending?
|
82
|
+
update_status!('expired')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def fail!
|
87
|
+
with_lock do
|
88
|
+
raise MailDeliveryTask::InvalidStateError unless pending?
|
89
|
+
update_status!('failed')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def may_schedule?
|
94
|
+
scheduled_at.blank? || scheduled_at < Time.current
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def deliver_mail(mail)
|
100
|
+
mail.deliver
|
101
|
+
end
|
102
|
+
|
103
|
+
# Override this if needed.
|
104
|
+
def persist_mail(mail)
|
105
|
+
raise NotImplementedError
|
106
|
+
end
|
107
|
+
|
108
|
+
# Override this if needed.
|
109
|
+
def handle_deliver_mail_error(e)
|
110
|
+
raise(e)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Override this if needed.
|
114
|
+
def handle_persist_mail_error(e)
|
115
|
+
raise(e)
|
116
|
+
end
|
117
|
+
|
118
|
+
def update_status!(status)
|
119
|
+
update!(
|
120
|
+
status: status,
|
121
|
+
completed_at: Time.current,
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MailDeliveryTask
|
2
|
+
module BaseDeliveryBatchJob
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include MailDeliveryTask::BaseJob
|
7
|
+
queue_as :default
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
unless_already_executing do
|
12
|
+
::MailDeliveryTask::Attempt.pending.where('scheduled_at IS ? || scheduled_at < ?', nil, Time.current).find_each do |task|
|
13
|
+
::MailDeliveryJob.perform_later(task)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module MailDeliveryTask
|
2
|
+
module BaseDeliveryJob
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include MailDeliveryTask::BaseJob
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
# @override
|
10
|
+
private def lock_key
|
11
|
+
[self.class.name, @task.id]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform(task)
|
16
|
+
@task = task
|
17
|
+
|
18
|
+
unless_already_executing do
|
19
|
+
@task.deliver! if @task.reload.pending?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module MailDeliveryTask
|
2
|
+
module BaseJob
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def lock_key
|
8
|
+
self.class.name
|
9
|
+
end
|
10
|
+
|
11
|
+
def unless_already_executing(&block)
|
12
|
+
result = ActiveRecord::Base.with_advisory_lock_result(lock_key, timeout_seconds: 0, &block)
|
13
|
+
warn("AdvisoryLock owned by other instance of job: #{lock_key}. Exiting.") unless result.lock_was_acquired?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/testing/**/*.rb"].each { |file| require file }
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module MailDeliveryTask
|
2
|
+
module Testing
|
3
|
+
module MailerHelper
|
4
|
+
extend RSpec::Matchers::DSL
|
5
|
+
|
6
|
+
# Tests that a provided mail object is deliverable.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# expect(mail).to be_deliverable
|
11
|
+
#
|
12
|
+
matcher :be_deliverable do
|
13
|
+
match do |actual|
|
14
|
+
expect { actual.deliver_now }.to change { ActionMailer::Base.deliveries.count }.by(1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Tests that a provided mail object has the expected attributes.
|
19
|
+
#
|
20
|
+
# Usage:
|
21
|
+
#
|
22
|
+
# expect(mail).to have_mailer_attributes(
|
23
|
+
# from: 'from@.squareup.com',
|
24
|
+
# reply_to: 'reply_to@.squareup.com',
|
25
|
+
# to: ['person_1@squareup.com', 'person_2@squareup.com'],
|
26
|
+
# bcc: 'bcc@squareup.com',
|
27
|
+
# subject: 'My random subject',
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
matcher :have_mailer_attributes do |from:, reply_to: nil, to:, bcc: nil, subject:|
|
31
|
+
match(notify_expectation_failures: true) do |actual|
|
32
|
+
expect(actual).to have_attributes(
|
33
|
+
from: Array(from),
|
34
|
+
reply_to: Array(reply_to),
|
35
|
+
to: Array(to),
|
36
|
+
bcc: Array(bcc),
|
37
|
+
subject: subject,
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tests that a string matches the content of a fixture file. Also provides
|
43
|
+
# convenience methods to be able to update the given fixture.
|
44
|
+
#
|
45
|
+
# Set `update_fixture` to true in order to automatically overwrite existing
|
46
|
+
# fixture files with latest expected text. Note that tests will *always*
|
47
|
+
# fail as long as `update_fixture` is set to true.
|
48
|
+
#
|
49
|
+
# Usage:
|
50
|
+
#
|
51
|
+
# expect('my_random_text').to match_fixture('spec/fixtures/dummy.html')
|
52
|
+
# expect('my_random_text').to match_fixture('spec/fixtures/dummy.html', update_fixture: true)
|
53
|
+
#
|
54
|
+
matcher :match_fixture do |fixture_file, update_fixture: false|
|
55
|
+
match do |actual|
|
56
|
+
# Trim all trailing whitespace from the actual generated (to avoid
|
57
|
+
# codebase from having any trailing whitespace)
|
58
|
+
trimmed_actual = actual.gsub(/[ \t]+$/, '')
|
59
|
+
|
60
|
+
if update_fixture
|
61
|
+
# Update the fixture file with actual content
|
62
|
+
path = File.dirname(fixture_file)
|
63
|
+
FileUtils.mkdir_p(path)
|
64
|
+
FileUtils.touch(fixture_file)
|
65
|
+
File.open(fixture_file, 'w+') do |file|
|
66
|
+
file.write(trimmed_actual)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Make sure the spec fails
|
70
|
+
false
|
71
|
+
else
|
72
|
+
begin
|
73
|
+
expect(trimmed_actual).to eq(File.read(fixture_file))
|
74
|
+
rescue
|
75
|
+
# Make sure the spec fails
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
failure_message do |actual|
|
82
|
+
if update_fixture
|
83
|
+
'Expected update_fixture to be false'
|
84
|
+
else
|
85
|
+
differ = RSpec::Support::Differ.new(
|
86
|
+
object_preparer: proc { |o| RSpec::Matchers::Composable.surface_descriptions_in(o) },
|
87
|
+
color: RSpec::Matchers.configuration.color?,
|
88
|
+
)
|
89
|
+
begin
|
90
|
+
expected = File.read(fixture_file)
|
91
|
+
<<~MESSAGE
|
92
|
+
expected: #{expected.inspect}
|
93
|
+
got: #{actual.inspect}
|
94
|
+
|
95
|
+
Diff: #{differ.diff(actual, expected)}
|
96
|
+
|
97
|
+
To update existing fixtures, toggle the `update_fixture` option.
|
98
|
+
|
99
|
+
Example: match_fixture('spec/fixtures/dummy.html', update_fixture: true)
|
100
|
+
MESSAGE
|
101
|
+
rescue
|
102
|
+
<<~MESSAGE
|
103
|
+
Fixture file #{fixture_file} does not exist yet. Please
|
104
|
+
generate fixtures using the `update_fixture` option.
|
105
|
+
|
106
|
+
Example: match_fixture('spec/fixtures/dummy.html', update_fixture: true)
|
107
|
+
MESSAGE
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mail_delivery_task/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'mail_delivery_task'
|
8
|
+
spec.version = MailDeliveryTask::VERSION
|
9
|
+
spec.authors = ['James Chang']
|
10
|
+
spec.email = ['jchang@squareup.com']
|
11
|
+
|
12
|
+
spec.summary = 'Async email delivery'
|
13
|
+
spec.homepage = 'https://github.com/square/mail_delivery_task'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.required_ruby_version = '>= 2.3'
|
21
|
+
|
22
|
+
spec.add_runtime_dependency 'activejob', '>= 4.2.0', '< 5.2'
|
23
|
+
spec.add_runtime_dependency 'activerecord', '>= 4.2.0', '< 5.2'
|
24
|
+
spec.add_runtime_dependency 'activesupport', '>= 4.2.0', '< 5.2'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'enumerize'
|
27
|
+
spec.add_runtime_dependency 'with_advisory_lock'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mail_delivery_task
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Chang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5.2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 4.2.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activerecord
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 4.2.0
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '5.2'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 4.2.0
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '5.2'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: activesupport
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 4.2.0
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.2'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 4.2.0
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '5.2'
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: enumerize
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
type: :runtime
|
81
|
+
prerelease: false
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: with_advisory_lock
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
type: :runtime
|
95
|
+
prerelease: false
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
description:
|
102
|
+
email:
|
103
|
+
- jchang@squareup.com
|
104
|
+
executables:
|
105
|
+
- console
|
106
|
+
- setup
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rspec"
|
112
|
+
- ".rubocop.yml"
|
113
|
+
- CHANGES.md
|
114
|
+
- Gemfile
|
115
|
+
- LICENSE
|
116
|
+
- README.md
|
117
|
+
- Rakefile
|
118
|
+
- bin/console
|
119
|
+
- bin/setup
|
120
|
+
- lib/generators/mail_delivery_task/install_generator.rb
|
121
|
+
- lib/generators/mail_delivery_task/templates/create_mail_delivery_task_attempts.rb
|
122
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_batch_job.rb.erb
|
123
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_batch_job_spec.rb.erb
|
124
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_job.rb.erb
|
125
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_job_spec.rb.erb
|
126
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_task_attempt.rb.erb
|
127
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_task_attempt_spec.rb.erb
|
128
|
+
- lib/generators/mail_delivery_task/templates/mail_delivery_task_attempts.rb.erb
|
129
|
+
- lib/mail_delivery_task.rb
|
130
|
+
- lib/mail_delivery_task/base_attempt.rb
|
131
|
+
- lib/mail_delivery_task/jobs/base_delivery_batch_job.rb
|
132
|
+
- lib/mail_delivery_task/jobs/base_delivery_job.rb
|
133
|
+
- lib/mail_delivery_task/jobs/base_job.rb
|
134
|
+
- lib/mail_delivery_task/testing.rb
|
135
|
+
- lib/mail_delivery_task/testing/mailer_helper.rb
|
136
|
+
- lib/mail_delivery_task/version.rb
|
137
|
+
- mail_delivery_task.gemspec
|
138
|
+
homepage: https://github.com/square/mail_delivery_task
|
139
|
+
licenses: []
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '2.3'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.5.1
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: Async email delivery
|
161
|
+
test_files: []
|