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.
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.1.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: 1980-01-02 00:00:00.000000000 Z
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.6.9
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: []