bouncy 0.1.0 → 0.2.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 +24 -0
- data/README.md +185 -93
- data/guides/README.md +20 -0
- data/guides/admin.md +2 -2
- data/guides/amazon-ses.md +70 -9
- data/guides/api.md +129 -0
- data/guides/bounce-handling.md +38 -1
- data/guides/compatibility.md +16 -2
- data/guides/configuration.md +83 -0
- data/guides/delivery.md +46 -8
- data/guides/migrating.md +3 -3
- data/guides/operations.md +104 -0
- data/guides/privacy.md +1 -1
- data/guides/troubleshooting.md +1 -1
- data/lib/bouncy/address_lock.rb +321 -0
- data/lib/bouncy/body_limit.rb +55 -0
- data/lib/bouncy/configuration.rb +38 -2
- data/lib/bouncy/event.rb +8 -3
- data/lib/bouncy/ingestor.rb +5 -3
- data/lib/bouncy/interceptor.rb +85 -16
- data/lib/bouncy/notification.rb +70 -0
- data/lib/bouncy/providers/ses.rb +57 -2
- data/lib/bouncy/providers/ses_parser.rb +20 -0
- data/lib/bouncy/providers/sns_verifier.rb +74 -6
- data/lib/bouncy/prune_job.rb +3 -0
- data/lib/bouncy/scope_lock.rb +3 -2
- data/lib/bouncy/store.rb +4 -0
- data/lib/bouncy/sync_job.rb +4 -0
- data/lib/bouncy/version.rb +1 -1
- data/lib/bouncy/webhook.rb +60 -9
- data/lib/bouncy.rb +34 -2
- data/lib/generators/bouncy/templates/initializer.rb.tt +14 -0
- metadata +13 -3
data/lib/bouncy.rb
CHANGED
|
@@ -84,7 +84,7 @@ module Bouncy
|
|
|
84
84
|
return Status.new(nil, knowledge: :unconfigured)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
Status.new(Suppression.find_by(scope: scope, email: Identity.normalize(email)))
|
|
87
|
+
Status.new(current_state(Suppression).find_by(scope: scope, email: Identity.normalize(email)))
|
|
88
88
|
rescue ActiveRecord::ActiveRecordError => e
|
|
89
89
|
raise unless database_unavailable?(e)
|
|
90
90
|
|
|
@@ -106,7 +106,7 @@ module Bouncy
|
|
|
106
106
|
return StatusSet.new(keys.index_with { Status.new(nil, knowledge: :unconfigured) }, knowledge: :unconfigured)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
rows = Suppression.where(scope: scope, email: keys).index_by(&:email)
|
|
109
|
+
rows = current_state(Suppression).where(scope: scope, email: keys).index_by(&:email)
|
|
110
110
|
StatusSet.new(keys.to_h { |key| [key, Status.new(rows[key])] })
|
|
111
111
|
rescue ActiveRecord::ActiveRecordError => e
|
|
112
112
|
raise unless database_unavailable?(e)
|
|
@@ -141,6 +141,35 @@ module Bouncy
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
# Holds the address while the block runs, so no new block for it can commit between the
|
|
145
|
+
# app's own check and its send. Inside a transaction the hold lasts until the transaction
|
|
146
|
+
# ends. Returns the block's value. Unconfigured, it only runs the block. The check needs the
|
|
147
|
+
# latest committed state: see current_state. Inside a PostgreSQL REPEATABLE READ or
|
|
148
|
+
# SERIALIZABLE transaction it raises; hold around such a transaction, and around SQLite ones.
|
|
149
|
+
#
|
|
150
|
+
# Bouncy.hold(order.email) do
|
|
151
|
+
# ReceiptMailer.with(order:).receipt.deliver_now unless Bouncy.blocked?(order.email)
|
|
152
|
+
# end
|
|
153
|
+
def hold(email, &)
|
|
154
|
+
raise ArgumentError, "Bouncy.hold needs a block" unless block_given?
|
|
155
|
+
|
|
156
|
+
unless configured?
|
|
157
|
+
unconfigured!("hold")
|
|
158
|
+
return yield
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
AddressLock.hold(email, &)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Inside Bouncy.hold, MySQL reads restrictions with a locking read, which sees the latest
|
|
165
|
+
# committed state: a REPEATABLE READ snapshot from before the hold cannot hide a newer block
|
|
166
|
+
# or release. PostgreSQL at READ COMMITTED reads the latest state with every statement.
|
|
167
|
+
def current_state(relation)
|
|
168
|
+
return relation unless AddressLock.holding? && Record.connection_pool.with_connection(&:adapter_name).match?(/Mysql|Trilogy/i)
|
|
169
|
+
|
|
170
|
+
relation.lock("LOCK IN SHARE MODE")
|
|
171
|
+
end
|
|
172
|
+
|
|
144
173
|
def unblocked
|
|
145
174
|
previous = ActiveSupport::IsolatedExecutionState[:bouncy_unblocked]
|
|
146
175
|
ActiveSupport::IsolatedExecutionState[:bouncy_unblocked] = true
|
|
@@ -169,10 +198,12 @@ require_relative "bouncy/suppression"
|
|
|
169
198
|
require_relative "bouncy/event"
|
|
170
199
|
require_relative "bouncy/status"
|
|
171
200
|
require_relative "bouncy/status_set"
|
|
201
|
+
require_relative "bouncy/address_lock"
|
|
172
202
|
require_relative "bouncy/store"
|
|
173
203
|
require_relative "bouncy/providers/base"
|
|
174
204
|
require_relative "bouncy/providers/ses"
|
|
175
205
|
require_relative "bouncy/providers/ses_parser"
|
|
206
|
+
require_relative "bouncy/notification"
|
|
176
207
|
require_relative "bouncy/ingestor"
|
|
177
208
|
require_relative "bouncy/recovery"
|
|
178
209
|
require_relative "bouncy/scope_lock"
|
|
@@ -180,6 +211,7 @@ require_relative "bouncy/reconciler"
|
|
|
180
211
|
require_relative "bouncy/model"
|
|
181
212
|
require_relative "bouncy/interceptor"
|
|
182
213
|
require_relative "bouncy/webhook"
|
|
214
|
+
require_relative "bouncy/body_limit"
|
|
183
215
|
require_relative "bouncy/sync_job"
|
|
184
216
|
require_relative "bouncy/prune_job"
|
|
185
217
|
require_relative "bouncy/engine"
|
|
@@ -19,5 +19,19 @@ Bouncy.configure do |config|
|
|
|
19
19
|
# config.soft_bounce_window = 30.days
|
|
20
20
|
# config.soft_bounce_block_for = 30.days
|
|
21
21
|
|
|
22
|
+
# Stamp one SES configuration set on every message, so SES publishes its events to the topic.
|
|
23
|
+
# config.ses.configuration_set = "transactional"
|
|
24
|
+
|
|
25
|
+
# Confirm the SNS subscription by visiting its signed SubscribeURL instead of calling
|
|
26
|
+
# sns:ConfirmSubscription.
|
|
27
|
+
# config.ses.subscription_confirmation = :subscribe_url
|
|
28
|
+
|
|
29
|
+
# An app that runs no sync job enforces its webhook blocks alone. webhook_blocking then
|
|
30
|
+
# defaults to :strict: every Permanent bounce and every complained recipient blocks.
|
|
31
|
+
# config.require_fresh_sync = false
|
|
32
|
+
|
|
33
|
+
# Every SES notification, after Bouncy recorded it. A raise makes SNS deliver it again.
|
|
34
|
+
# config.on_notification = ->(notification) { }
|
|
35
|
+
|
|
22
36
|
config.interception = :log # Review a complete sync before enabling :drop.
|
|
23
37
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bouncy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-25 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -61,16 +62,22 @@ files:
|
|
|
61
62
|
- LICENSE.txt
|
|
62
63
|
- README.md
|
|
63
64
|
- config/routes.rb
|
|
65
|
+
- guides/README.md
|
|
64
66
|
- guides/admin.md
|
|
65
67
|
- guides/amazon-ses.md
|
|
68
|
+
- guides/api.md
|
|
66
69
|
- guides/bounce-handling.md
|
|
67
70
|
- guides/compatibility.md
|
|
71
|
+
- guides/configuration.md
|
|
68
72
|
- guides/delivery.md
|
|
69
73
|
- guides/migrating.md
|
|
74
|
+
- guides/operations.md
|
|
70
75
|
- guides/privacy.md
|
|
71
76
|
- guides/recovery.md
|
|
72
77
|
- guides/troubleshooting.md
|
|
73
78
|
- lib/bouncy.rb
|
|
79
|
+
- lib/bouncy/address_lock.rb
|
|
80
|
+
- lib/bouncy/body_limit.rb
|
|
74
81
|
- lib/bouncy/configuration.rb
|
|
75
82
|
- lib/bouncy/engine.rb
|
|
76
83
|
- lib/bouncy/event.rb
|
|
@@ -78,6 +85,7 @@ files:
|
|
|
78
85
|
- lib/bouncy/ingestor.rb
|
|
79
86
|
- lib/bouncy/interceptor.rb
|
|
80
87
|
- lib/bouncy/model.rb
|
|
88
|
+
- lib/bouncy/notification.rb
|
|
81
89
|
- lib/bouncy/providers/base.rb
|
|
82
90
|
- lib/bouncy/providers/ses.rb
|
|
83
91
|
- lib/bouncy/providers/ses_parser.rb
|
|
@@ -106,6 +114,7 @@ metadata:
|
|
|
106
114
|
source_code_uri: https://github.com/rameerez/bouncy/tree/main
|
|
107
115
|
changelog_uri: https://github.com/rameerez/bouncy/blob/main/CHANGELOG.md
|
|
108
116
|
rubygems_mfa_required: 'true'
|
|
117
|
+
post_install_message:
|
|
109
118
|
rdoc_options: []
|
|
110
119
|
require_paths:
|
|
111
120
|
- lib
|
|
@@ -120,7 +129,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
129
|
- !ruby/object:Gem::Version
|
|
121
130
|
version: '0'
|
|
122
131
|
requirements: []
|
|
123
|
-
rubygems_version: 3.
|
|
132
|
+
rubygems_version: 3.5.22
|
|
133
|
+
signing_key:
|
|
124
134
|
specification_version: 4
|
|
125
135
|
summary: Email bounce handling and suppression management for Rails.
|
|
126
136
|
test_files: []
|