broadcast-ruby 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 798e8750d07fb38ea8ab9b79b3bdbcf9f3c267a09943d2aa49fc87d03b18eacb
4
- data.tar.gz: 7113b7abf01a71d1715c10e1b4170e9267426e12b304fff4c61c8ce15a6d457c
3
+ metadata.gz: 8f76251ad3cf3d351bd4b197ccc5b4035af1eea66290380050c6f5108ec48b6a
4
+ data.tar.gz: 85c3e88178863291ab1da149797f71ea36a7eeaf3418bb72bdcf95ab92dfcf7e
5
5
  SHA512:
6
- metadata.gz: d92a6f509c322da7e7af7b538c583dffc11eb6cda4abe424da7e0c10a4bad842149416b036f728568e7f2c92815620ed929894e4cbd765b06402d14ef4cfec53
7
- data.tar.gz: a543c779489d537034c52dd5f73db4482fea1268fe250858a601ee7aefa6f4829d330f593a1e8134314de7511d5b283def5fea229d366f7d0ef9435935f65261
6
+ metadata.gz: 5d71f0add4f5ae7b00705753069c6eee46831c2a1a9b7af36f7dc7cd27d522100d471923a3450a2cd5bab6897b567ace37c127d8e9df16e7e9cef57f53f5a55e
7
+ data.tar.gz: f31f85327c7c231c42d5134961387b5da21d2cc6a9080ca8d31061d5be540a4c38f6e4a9641a4c66427767a2ba24129d746f5ba0ebcebf5a4f320b0d12ea29ca
data/Gemfile CHANGED
@@ -4,6 +4,8 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'actionmailer', '>= 6.0'
8
+ gem 'mail', '~> 2.8'
7
9
  gem 'minitest', '~> 5.0'
8
10
  gem 'rake', '~> 13.0'
9
11
  gem 'rubocop', '~> 1.0'
data/Gemfile.lock CHANGED
@@ -7,24 +7,96 @@ PATH
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
+ actionmailer (8.1.2)
11
+ actionpack (= 8.1.2)
12
+ actionview (= 8.1.2)
13
+ activejob (= 8.1.2)
14
+ activesupport (= 8.1.2)
15
+ mail (>= 2.8.0)
16
+ rails-dom-testing (~> 2.2)
17
+ actionpack (8.1.2)
18
+ actionview (= 8.1.2)
19
+ activesupport (= 8.1.2)
20
+ nokogiri (>= 1.8.5)
21
+ rack (>= 2.2.4)
22
+ rack-session (>= 1.0.1)
23
+ rack-test (>= 0.6.3)
24
+ rails-dom-testing (~> 2.2)
25
+ rails-html-sanitizer (~> 1.6)
26
+ useragent (~> 0.16)
27
+ actionview (8.1.2)
28
+ activesupport (= 8.1.2)
29
+ builder (~> 3.1)
30
+ erubi (~> 1.11)
31
+ rails-dom-testing (~> 2.2)
32
+ rails-html-sanitizer (~> 1.6)
33
+ activejob (8.1.2)
34
+ activesupport (= 8.1.2)
35
+ globalid (>= 0.3.6)
36
+ activesupport (8.1.2)
37
+ base64
38
+ bigdecimal
39
+ concurrent-ruby (~> 1.0, >= 1.3.1)
40
+ connection_pool (>= 2.2.5)
41
+ drb
42
+ i18n (>= 1.6, < 2)
43
+ json
44
+ logger (>= 1.4.2)
45
+ minitest (>= 5.1)
46
+ securerandom (>= 0.3)
47
+ tzinfo (~> 2.0, >= 2.0.5)
48
+ uri (>= 0.13.1)
10
49
  addressable (2.8.9)
11
50
  public_suffix (>= 2.0.2, < 8.0)
12
51
  ast (2.4.3)
13
52
  base64 (0.3.0)
14
53
  bigdecimal (4.0.1)
54
+ builder (3.3.0)
55
+ concurrent-ruby (1.3.6)
56
+ connection_pool (3.0.2)
15
57
  crack (1.0.1)
16
58
  bigdecimal
17
59
  rexml
60
+ crass (1.0.6)
61
+ date (3.5.1)
62
+ drb (2.2.3)
63
+ erubi (1.13.1)
64
+ globalid (1.3.0)
65
+ activesupport (>= 6.1)
18
66
  hashdiff (1.2.1)
67
+ i18n (1.14.8)
68
+ concurrent-ruby (~> 1.0)
19
69
  json (2.19.1)
20
70
  json-schema (6.2.0)
21
71
  addressable (~> 2.8)
22
72
  bigdecimal (>= 3.1, < 5)
23
73
  language_server-protocol (3.17.0.5)
24
74
  lint_roller (1.1.0)
75
+ logger (1.7.0)
76
+ loofah (2.25.1)
77
+ crass (~> 1.0.2)
78
+ nokogiri (>= 1.12.0)
79
+ mail (2.9.0)
80
+ logger
81
+ mini_mime (>= 0.1.1)
82
+ net-imap
83
+ net-pop
84
+ net-smtp
25
85
  mcp (0.8.0)
26
86
  json-schema (>= 4.1)
87
+ mini_mime (1.1.5)
27
88
  minitest (5.27.0)
89
+ net-imap (0.6.3)
90
+ date
91
+ net-protocol
92
+ net-pop (0.1.2)
93
+ net-protocol
94
+ net-protocol (0.2.2)
95
+ timeout
96
+ net-smtp (0.5.1)
97
+ net-protocol
98
+ nokogiri (1.19.1-arm64-darwin)
99
+ racc (~> 1.4)
28
100
  parallel (1.27.0)
29
101
  parser (3.3.10.2)
30
102
  ast (~> 2.4.1)
@@ -32,6 +104,19 @@ GEM
32
104
  prism (1.9.0)
33
105
  public_suffix (7.0.5)
34
106
  racc (1.8.1)
107
+ rack (3.2.5)
108
+ rack-session (2.1.1)
109
+ base64 (>= 0.1.0)
110
+ rack (>= 3.0.0)
111
+ rack-test (2.2.0)
112
+ rack (>= 1.3)
113
+ rails-dom-testing (2.3.0)
114
+ activesupport (>= 5.0.0)
115
+ minitest
116
+ nokogiri (>= 1.6)
117
+ rails-html-sanitizer (1.7.0)
118
+ loofah (~> 2.25)
119
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
35
120
  rainbow (3.1.1)
36
121
  rake (13.3.1)
37
122
  regexp_parser (2.11.3)
@@ -52,9 +137,15 @@ GEM
52
137
  parser (>= 3.3.7.2)
53
138
  prism (~> 1.7)
54
139
  ruby-progressbar (1.13.0)
140
+ securerandom (0.4.1)
141
+ timeout (0.6.1)
142
+ tzinfo (2.0.6)
143
+ concurrent-ruby (~> 1.0)
55
144
  unicode-display_width (3.2.0)
56
145
  unicode-emoji (~> 4.1)
57
146
  unicode-emoji (4.2.0)
147
+ uri (1.1.1)
148
+ useragent (0.16.11)
58
149
  webmock (3.26.1)
59
150
  addressable (>= 2.8.0)
60
151
  crack (>= 0.3.2)
@@ -62,34 +153,63 @@ GEM
62
153
 
63
154
  PLATFORMS
64
155
  arm64-darwin-24
65
- ruby
66
156
 
67
157
  DEPENDENCIES
158
+ actionmailer (>= 6.0)
68
159
  broadcast-ruby!
160
+ mail (~> 2.8)
69
161
  minitest (~> 5.0)
70
162
  rake (~> 13.0)
71
163
  rubocop (~> 1.0)
72
164
  webmock (~> 3.0)
73
165
 
74
166
  CHECKSUMS
167
+ actionmailer (8.1.2) sha256=f4c1d2060f653bfe908aa7fdc5a61c0e5279670de992146582f2e36f8b9175e9
168
+ actionpack (8.1.2) sha256=ced74147a1f0daafaa4bab7f677513fd4d3add574c7839958f7b4f1de44f8423
169
+ actionview (8.1.2) sha256=80455b2588911c9b72cec22d240edacb7c150e800ef2234821269b2b2c3e2e5b
170
+ activejob (8.1.2) sha256=908dab3713b101859536375819f4156b07bdf4c232cc645e7538adb9e302f825
171
+ activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae
75
172
  addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485
76
173
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
77
174
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
78
175
  bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
79
176
  broadcast-ruby (0.1.0)
177
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
178
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
179
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
80
180
  crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
181
+ crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
182
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
183
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
184
+ erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
185
+ globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
81
186
  hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
187
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
82
188
  json (2.19.1) sha256=dd94fdc59e48bff85913829a32350b3148156bc4fd2a95a2568a78b11344082d
83
189
  json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666
84
190
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
85
191
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
192
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
193
+ loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04
194
+ mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
86
195
  mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
196
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
87
197
  minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
198
+ net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
199
+ net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
200
+ net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
201
+ net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
202
+ nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e
88
203
  parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
89
204
  parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
90
205
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
91
206
  public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
92
207
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
208
+ rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
209
+ rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
210
+ rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
211
+ rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
212
+ rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89
93
213
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
94
214
  rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
95
215
  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
@@ -97,8 +217,13 @@ CHECKSUMS
97
217
  rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77
98
218
  rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
99
219
  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
220
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
221
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
222
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
100
223
  unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
101
224
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
225
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
226
+ useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
102
227
  webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7
103
228
 
104
229
  BUNDLED WITH
data/README.md CHANGED
@@ -6,13 +6,35 @@ Works with [sendbroadcast.com](https://sendbroadcast.com) or any self-hosted Bro
6
6
 
7
7
  ## Installation
8
8
 
9
+ Add to your Gemfile:
10
+
9
11
  ```ruby
10
12
  gem 'broadcast-ruby'
11
13
  ```
12
14
 
15
+ For plain Ruby scripts (non-Rails):
16
+
17
+ ```ruby
18
+ require 'broadcast'
19
+ ```
20
+
21
+ ## Getting Your API Token
22
+
23
+ 1. Log in to your Broadcast dashboard
24
+ 2. Go to **Settings > API Keys**
25
+ 3. Click **New API Key**
26
+ 4. Name it, select the permissions you need (see [Permissions](#api-token-permissions) below), and save
27
+ 5. Copy the token
28
+
13
29
  ## Quick Start
14
30
 
31
+ **In a Rails app?** Jump to [Rails ActionMailer Integration](#rails-actionmailer-integration) -- you don't need to use the client directly.
32
+
33
+ **In a plain Ruby script or non-Rails app?** Use the client:
34
+
15
35
  ```ruby
36
+ require 'broadcast'
37
+
16
38
  client = Broadcast::Client.new(
17
39
  api_token: 'your-token',
18
40
  host: 'https://sendbroadcast.com' # or your self-hosted URL
@@ -44,11 +66,91 @@ All methods return parsed JSON as Ruby Hashes with string keys.
44
66
 
45
67
  ---
46
68
 
69
+ ## Rails ActionMailer Integration
70
+
71
+ In a Rails app, the gem auto-registers a `:broadcast` delivery method. Your existing mailers work unchanged.
72
+
73
+ First, store your API token in Rails credentials:
74
+
75
+ ```bash
76
+ bin/rails credentials:edit
77
+ ```
78
+
79
+ Add:
80
+
81
+ ```yaml
82
+ broadcast:
83
+ api_token: your-token-here
84
+ ```
85
+
86
+ Then configure production to use Broadcast:
87
+
88
+ ```ruby
89
+ # config/environments/production.rb
90
+ config.action_mailer.delivery_method = :broadcast
91
+ config.action_mailer.broadcast_settings = {
92
+ api_token: Rails.application.credentials.dig(:broadcast, :api_token),
93
+ host: 'https://sendbroadcast.com'
94
+ }
95
+ ```
96
+
97
+ All your mailers just work:
98
+
99
+ ```ruby
100
+ UserMailer.welcome(user).deliver_later
101
+ PasswordsMailer.reset(user).deliver_now
102
+ ```
103
+
104
+ The delivery method extracts the HTML body (falling back to text), subject, recipient, and reply-to from the rendered email and sends it via Broadcast's transactional API. Delivery errors raise `Broadcast::DeliveryError`, which lets Active Job (Solid Queue, Sidekiq, etc.) retry automatically.
105
+
106
+ Development and test environments are unaffected:
107
+
108
+ ```ruby
109
+ # config/environments/development.rb
110
+ config.action_mailer.delivery_method = :letter_opener
111
+
112
+ # config/environments/test.rb
113
+ config.action_mailer.delivery_method = :test
114
+ ```
115
+
116
+ ### Migrating from Postmark
117
+
118
+ Replace in your Gemfile:
119
+
120
+ ```diff
121
+ - gem 'postmark-rails'
122
+ + gem 'broadcast-ruby'
123
+ ```
124
+
125
+ Replace in `config/environments/production.rb`:
126
+
127
+ ```diff
128
+ - config.action_mailer.delivery_method = :postmark
129
+ - config.action_mailer.postmark_settings = {
130
+ - api_token: Rails.application.credentials.dig(:postmark, :api_token)
131
+ - }
132
+ + config.action_mailer.delivery_method = :broadcast
133
+ + config.action_mailer.broadcast_settings = {
134
+ + api_token: Rails.application.credentials.dig(:broadcast, :api_token),
135
+ + host: 'https://sendbroadcast.com'
136
+ + }
137
+ ```
138
+
139
+ Your mailer classes, views, and tests stay exactly the same.
140
+
141
+ ### Migrating from SendGrid / Mailgun
142
+
143
+ Same pattern -- swap the gem and the delivery config. No changes to mailers.
144
+
145
+ ---
146
+
47
147
  ## Transactional Email
48
148
 
49
149
  Send one-off emails triggered by application events. Transactional emails bypass unsubscribe status (for password resets, receipts, order confirmations, etc.).
50
150
 
51
- The sender address is configured at the channel level in your Broadcast instance there is no `from` parameter.
151
+ The sender address is configured at the channel level in your Broadcast instance -- there is no `from` parameter.
152
+
153
+ **Required permissions:** `transactionals_read`, `transactionals_write`
52
154
 
53
155
  ```ruby
54
156
  # Send an email
@@ -86,6 +188,8 @@ email['queue_at'] # => '2026-03-17T07:59:58Z'
86
188
 
87
189
  Manage your contact list. Subscribers can have tags and custom data fields for segmentation and personalization.
88
190
 
191
+ **Required permissions:** `subscribers_read`, `subscribers_write`
192
+
89
193
  ```ruby
90
194
  # List subscribers (paginated, 250 per page)
91
195
  result = client.subscribers.list(page: 1)
@@ -154,7 +258,7 @@ client.subscribers.unsubscribe('jane@example.com')
154
258
  # Resubscribe (clears unsubscribed status AND activates)
155
259
  client.subscribers.resubscribe('jane@example.com')
156
260
 
157
- # Redact GDPR "right to be forgotten" (irreversible)
261
+ # Redact -- GDPR "right to be forgotten" (irreversible)
158
262
  # Removes PII but preserves aggregate campaign statistics
159
263
  client.subscribers.redact('jane@example.com')
160
264
  ```
@@ -165,6 +269,8 @@ client.subscribers.redact('jane@example.com')
165
269
 
166
270
  Automated drip campaigns. Add subscribers to a sequence and they flow through the steps automatically.
167
271
 
272
+ **Required permissions:** `sequences_read`, `sequences_write` (also `subscribers_read`, `subscribers_write` for enrollment)
273
+
168
274
  ```ruby
169
275
  # List all sequences
170
276
  result = client.sequences.list
@@ -230,7 +336,7 @@ client.sequences.list_steps(sequence_id)
230
336
  # Get a single step
231
337
  step = client.sequences.get_step(sequence_id, step_id)
232
338
 
233
- # Create steps each step needs an action, label, and parent_id
339
+ # Create steps -- each step needs an action, label, and parent_id
234
340
  # The parent_id links steps into a tree (entry point is the sequence's root step)
235
341
 
236
342
  # Delay step (seconds)
@@ -279,6 +385,8 @@ client.sequences.delete_step(sequence_id, step_id)
279
385
 
280
386
  One-time email campaigns sent to your subscriber list or targeted segments.
281
387
 
388
+ **Required permissions:** `broadcasts_read`, `broadcasts_write`
389
+
282
390
  ```ruby
283
391
  # List broadcasts
284
392
  result = client.broadcasts.list(limit: 10, offset: 0)
@@ -350,6 +458,8 @@ client.broadcasts.statistics_links(1, sort: 'clicks', order: 'desc')
350
458
 
351
459
  Define subscriber groups using rules for targeted broadcasts and sequence enrollment.
352
460
 
461
+ **Required permissions:** `segments_read`, `segments_write`
462
+
353
463
  ```ruby
354
464
  # List segments
355
465
  result = client.segments.list
@@ -401,6 +511,8 @@ client.segments.delete(1)
401
511
 
402
512
  Reusable email templates with [Liquid](https://shopify.github.io/liquid/) variable support for personalization (e.g. `{{first_name}}`, `{{email}}`).
403
513
 
514
+ **Required permissions:** `templates_read`, `templates_write`
515
+
404
516
  ```ruby
405
517
  # List all templates
406
518
  result = client.templates.list
@@ -435,6 +547,8 @@ client.templates.delete(1)
435
547
 
436
548
  Receive real-time notifications when events occur (email delivered, subscriber created, sequence completed, etc.).
437
549
 
550
+ **Required permissions:** `webhook_endpoints_read`, `webhook_endpoints_write`
551
+
438
552
  ```ruby
439
553
  # List endpoints
440
554
  result = client.webhook_endpoints.list
@@ -453,10 +567,10 @@ result = client.webhook_endpoints.create(
453
567
  ],
454
568
  retries_to_attempt: 6
455
569
  )
456
- # IMPORTANT: Save the secret from the response it is only shown once
570
+ # IMPORTANT: Save the secret from the response -- it is only shown once
457
571
  secret = result['secret']
458
572
 
459
- # Update (url and secret cannot be changed create a new endpoint instead)
573
+ # Update (url and secret cannot be changed -- create a new endpoint instead)
460
574
  client.webhook_endpoints.update(1, active: false)
461
575
  client.webhook_endpoints.update(1, event_types: ['email.delivered', 'email.opened'])
462
576
 
@@ -483,16 +597,35 @@ result['data'] # => [{'id' => 1, 'event_type' => 'email.sent', 'response_status
483
597
 
484
598
  ### Webhook Signature Verification
485
599
 
486
- All incoming webhooks are signed with HMAC-SHA256. Verify them in your controller:
600
+ All incoming webhooks are signed with HMAC-SHA256. Here's a complete Rails controller:
487
601
 
488
602
  ```ruby
489
- Broadcast::Webhook.verify(
490
- request.raw_post, # raw request body
491
- request.headers['broadcast-webhook-signature'], # v1,<base64 signature>
492
- request.headers['broadcast-webhook-timestamp'], # unix timestamp
493
- secret: ENV['BROADCAST_WEBHOOK_SECRET']
494
- )
495
- # => true or false
603
+ class WebhooksController < ApplicationController
604
+ skip_before_action :verify_authenticity_token
605
+
606
+ def create
607
+ payload = request.raw_post
608
+ signature = request.headers['broadcast-webhook-signature']
609
+ timestamp = request.headers['broadcast-webhook-timestamp']
610
+
611
+ unless Broadcast::Webhook.verify(payload, signature, timestamp,
612
+ secret: ENV['BROADCAST_WEBHOOK_SECRET'])
613
+ head :unauthorized
614
+ return
615
+ end
616
+
617
+ event = JSON.parse(payload)
618
+
619
+ case event['type']
620
+ when 'email.delivered'
621
+ # handle delivery
622
+ when 'subscriber.unsubscribed'
623
+ # handle unsubscribe
624
+ end
625
+
626
+ head :ok
627
+ end
628
+ end
496
629
  ```
497
630
 
498
631
  The signature is computed as `HMAC-SHA256(timestamp + "." + payload, secret)`. Timestamps older than 5 minutes are rejected to prevent replay attacks.
@@ -501,21 +634,74 @@ The signature is computed as `HMAC-SHA256(timestamp + "." + payload, secret)`. T
501
634
 
502
635
  ## Error Handling
503
636
 
504
- All API errors inherit from `Broadcast::Error`:
637
+ All API errors inherit from `Broadcast::Error`. Put specific errors before general ones:
505
638
 
506
639
  ```ruby
507
640
  begin
508
641
  client.send_email(to: 'user@example.com', subject: 'Hi', body: 'Hello')
509
- rescue Broadcast::AuthenticationError # 401 invalid or expired API token
510
- rescue Broadcast::NotFoundError # 404 resource does not exist
511
- rescue Broadcast::ValidationError # 422 missing or invalid parameters
512
- rescue Broadcast::RateLimitError # 429 exceeded 120 requests/minute
642
+ rescue Broadcast::AuthenticationError # 401 -- invalid or expired API token
643
+ rescue Broadcast::NotFoundError # 404 -- resource does not exist
644
+ rescue Broadcast::ValidationError # 422 -- missing or invalid parameters
645
+ rescue Broadcast::RateLimitError # 429 -- exceeded 120 requests/minute
513
646
  rescue Broadcast::TimeoutError # connection or read timeout
514
647
  rescue Broadcast::APIError # 5xx or unexpected status codes
648
+ rescue Broadcast::DeliveryError # ActionMailer wrapper (wraps any of the above)
515
649
  end
516
650
  ```
517
651
 
518
- Server errors (5xx) and timeouts are automatically retried with linear backoff. Client errors (401, 404, 422, 429) are raised immediately.
652
+ Server errors (5xx) and timeouts are automatically retried with linear backoff. Client errors (401, 404, 422, 429) are raised immediately. `DeliveryError` is only raised from the ActionMailer delivery method -- it wraps the underlying API error.
653
+
654
+ ---
655
+
656
+ ## API Token Permissions
657
+
658
+ Each token can be scoped to specific resources. The ActionMailer delivery method only needs transactional permissions. Use the minimum permissions your integration requires.
659
+
660
+ | Resource | Read permission | Write permission |
661
+ |----------|----------------|------------------|
662
+ | Transactional Emails | `transactionals_read` -- get delivery status | `transactionals_write` -- send emails |
663
+ | Subscribers | `subscribers_read` -- list, find | `subscribers_write` -- create, update, tag, deactivate, unsubscribe, redact |
664
+ | Sequences | `sequences_read` -- list, get, list steps | `sequences_write` -- create, update, delete, manage steps, enroll subscribers |
665
+ | Broadcasts | `broadcasts_read` -- list, get, statistics | `broadcasts_write` -- create, update, delete, send, schedule |
666
+ | Segments | `segments_read` -- list, get | `segments_write` -- create, update, delete |
667
+ | Templates | `templates_read` -- list, get | `templates_write` -- create, update, delete |
668
+ | Webhook Endpoints | `webhook_endpoints_read` -- list, get, deliveries | `webhook_endpoints_write` -- create, update, delete, test |
669
+
670
+ ---
671
+
672
+ ## Troubleshooting
673
+
674
+ ### `Broadcast::AuthenticationError` (401)
675
+
676
+ - **Wrong token:** Double-check you copied the full token from your Broadcast dashboard.
677
+ - **Wrong host:** If you're self-hosting, make sure `host` points to your Broadcast instance, not `sendbroadcast.com`.
678
+ - **Missing permissions:** Your token may not have the required permissions for the resource you're accessing. Check the [permissions table](#api-token-permissions).
679
+
680
+ ### `Broadcast::ValidationError` (422)
681
+
682
+ - **Missing required fields:** Check the parameters table for the method you're calling.
683
+ - **Duplicate subscriber:** Creating a subscriber with an email that already exists returns 422. Use `find` first or handle the error.
684
+ - **Duplicate template label:** Template labels must be unique within a channel.
685
+
686
+ ### `Broadcast::NotFoundError` (404)
687
+
688
+ - **Wrong ID:** The resource ID doesn't exist or belongs to a different channel.
689
+ - **Subscriber not found:** `subscribers.find(email: '...')` raises 404 if no subscriber matches.
690
+
691
+ ### `Broadcast::TimeoutError`
692
+
693
+ - **Slow network:** Increase `timeout` and `open_timeout` in client options.
694
+ - **Large response:** Template list responses can be large if templates contain full HTML bodies.
695
+
696
+ ### `Broadcast::RateLimitError` (429)
697
+
698
+ - **Rate limit:** Broadcast allows 120 requests per minute per token. The gem does not auto-retry on 429 -- back off and retry in your application code.
699
+
700
+ ### ActionMailer emails not sending
701
+
702
+ - **Check delivery method:** Ensure `config.action_mailer.delivery_method = :broadcast` is set in the right environment file (not development or test).
703
+ - **Check credentials:** Run `bin/rails credentials:show` and verify `broadcast.api_token` is set.
704
+ - **Check logs:** Set `debug: true` in `broadcast_settings` to see request/response details.
519
705
 
520
706
  ## License
521
707
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Broadcast
4
+ class DeliveryMethod
5
+ def initialize(settings = {})
6
+ @client = Client.new(**settings)
7
+ end
8
+
9
+ def deliver!(mail)
10
+ @client.send_email(
11
+ to: mail.to&.first,
12
+ subject: mail.subject,
13
+ body: extract_body(mail),
14
+ reply_to: mail.reply_to&.first
15
+ )
16
+ rescue Broadcast::Error => e
17
+ raise DeliveryError, "Failed to deliver email: #{e.message}"
18
+ end
19
+
20
+ private
21
+
22
+ def extract_body(mail)
23
+ if mail.html_part
24
+ mail.html_part.body.to_s
25
+ elsif mail.text_part
26
+ mail.text_part.body.to_s
27
+ else
28
+ mail.body.to_s
29
+ end
30
+ end
31
+ end
32
+ end
@@ -16,4 +16,6 @@ module Broadcast
16
16
  class ValidationError < Error; end
17
17
 
18
18
  class TimeoutError < Error; end
19
+
20
+ class DeliveryError < Error; end
19
21
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'delivery_method'
4
+
5
+ module Broadcast
6
+ class Railtie < Rails::Railtie
7
+ initializer 'broadcast.add_delivery_method' do
8
+ ActiveSupport.on_load(:action_mailer) do
9
+ ActionMailer::Base.add_delivery_method :broadcast, Broadcast::DeliveryMethod
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Broadcast
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
data/lib/broadcast.rb CHANGED
@@ -13,5 +13,11 @@ require_relative 'broadcast/resources/segments'
13
13
  require_relative 'broadcast/resources/templates'
14
14
  require_relative 'broadcast/resources/webhook_endpoints'
15
15
 
16
+ # ActionMailer integration — only loaded when Rails is present
17
+ if defined?(Rails::Railtie)
18
+ require_relative 'broadcast/delivery_method'
19
+ require_relative 'broadcast/railtie'
20
+ end
21
+
16
22
  module Broadcast
17
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: broadcast-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiu
@@ -40,7 +40,9 @@ files:
40
40
  - lib/broadcast.rb
41
41
  - lib/broadcast/client.rb
42
42
  - lib/broadcast/configuration.rb
43
+ - lib/broadcast/delivery_method.rb
43
44
  - lib/broadcast/errors.rb
45
+ - lib/broadcast/railtie.rb
44
46
  - lib/broadcast/resources/base.rb
45
47
  - lib/broadcast/resources/broadcasts.rb
46
48
  - lib/broadcast/resources/segments.rb