broadcast-ruby 0.1.4 → 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/.rubocop.yml +2 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +2 -2
- data/README.md +310 -0
- data/lib/broadcast/client.rb +113 -42
- data/lib/broadcast/configuration.rb +3 -1
- data/lib/broadcast/errors.rb +2 -0
- data/lib/broadcast/resources/email_servers.rb +88 -0
- data/lib/broadcast/resources/opt_in_forms.rb +71 -0
- data/lib/broadcast/resources/subscribers.rb +19 -1
- data/lib/broadcast/resources/transactionals.rb +49 -0
- data/lib/broadcast/version.rb +1 -1
- data/lib/broadcast.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 361d356c1fde238ba1734363607796b005a839f72b70885e52c2ed0eaa2b4209
|
|
4
|
+
data.tar.gz: 9a1d12cc2b3680689951e325be2100eb64ed9f2ca021c3d69421f336099dec6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 128a65f8767c17b979e98b8516f7c31a99703449487f6fd7cefe93c9940a1c3cb5c4ae21a5144a7236769ec5f41072626694e169ec2eea041f3c1327265c834d
|
|
7
|
+
data.tar.gz: e54a4fa7d5f1e96f9fbc26a8932c05067a6eda7575ee21f3762cc261b0973d42219536ed1650872746ce2854fa449de9ebc3c0192445ab705ceec0933e36df1d
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-04-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `client.opt_in_forms` resource: list, get, create, update, delete, analytics, create_variant, duplicate
|
|
9
|
+
- `client.email_servers` resource: list, get, create, update, delete, test_connection, copy_to_channel
|
|
10
|
+
- `client.transactionals` resource with full create surface (`template_id`, `preheader`, `include_unsubscribe_link`, `subscriber:` attrs); `client.send_email` and `client.get_email` are now thin shims that delegate
|
|
11
|
+
- Double opt-in support: pass `double_opt_in: true` (or a hash with `reply_to:` / `confirmation_template_id:` / `include_unsubscribe_link:`) to `transactionals.create` and `subscribers.create`. Optional top-level `confirmation_template_id:` is also accepted
|
|
12
|
+
- `Configuration#broadcast_channel_id` plus `client.with_channel(id) { ... }` block API for admin/system tokens — auto-includes the channel on every request inside the block (or globally when set on config), without overriding callers that pass it explicitly
|
|
13
|
+
- `Broadcast::AuthorizationError` for 403 responses (previously fell through to a generic `APIError`)
|
|
14
|
+
- Credential redaction scrubber on `email_servers.update`: values matching the API's bullet-redaction shape on known credential fields are stripped from the payload (with a logger warning) so callers can't accidentally round-trip a redacted response back into the model
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
- `opt_in_forms.list` returns up to 250 results per page with `pagination` metadata; only main forms are returned (variants are excluded)
|
|
18
|
+
- `opt_in_forms` `index`/`show` JSON shape (rendered via JBuilder views) differs slightly from `create`/`update` (rendered via the controller's inline serializer)
|
|
19
|
+
- `email_servers.copy_to_channel` requires an admin token and is account-scoped in SaaS mode
|
|
20
|
+
|
|
21
|
+
## [0.1.4] - 2026-03-18
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Register delivery method at class load time instead of in an initializer. ActionMailer's railtie applies config settings (including `broadcast_settings`) inside `on_load(:action_mailer)`, which ran before our initializer-based registration. This caused `undefined method broadcast_settings=` on boot in Rails 8.1.
|
|
25
|
+
|
|
26
|
+
## [0.1.3] - 2026-03-18
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Attempted fix for Railtie timing: use `before: :load_config_initializers`. Did not fully resolve the issue — superseded by 0.1.4.
|
|
30
|
+
|
|
5
31
|
## [0.1.2] - 2026-03-18
|
|
6
32
|
|
|
7
33
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
broadcast-ruby (0.
|
|
4
|
+
broadcast-ruby (0.2.0)
|
|
5
5
|
base64
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -176,7 +176,7 @@ CHECKSUMS
|
|
|
176
176
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
177
177
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
178
178
|
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
|
179
|
-
broadcast-ruby (0.
|
|
179
|
+
broadcast-ruby (0.2.0)
|
|
180
180
|
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
|
181
181
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
182
182
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
data/README.md
CHANGED
|
@@ -59,6 +59,7 @@ client.send_email(
|
|
|
59
59
|
| `retry_delay` | `1` | Base delay between retries in seconds (multiplied by attempt number) |
|
|
60
60
|
| `debug` | `false` | Log request/response details |
|
|
61
61
|
| `logger` | `nil` | Logger instance for debug output (e.g. `Rails.logger`) |
|
|
62
|
+
| `broadcast_channel_id` | `nil` | Auto-included on every request when set. Required when using an admin/system token (regular tokens are channel-scoped already). Can be overridden per-call or via `client.with_channel(id) { ... }` |
|
|
62
63
|
|
|
63
64
|
All methods return parsed JSON as Ruby Hashes with string keys.
|
|
64
65
|
|
|
@@ -182,6 +183,75 @@ email['queue_at'] # => '2026-03-17T07:59:58Z'
|
|
|
182
183
|
| `body` | yes | Email content (HTML or plain text) |
|
|
183
184
|
| `reply_to` | no | Reply-to address |
|
|
184
185
|
|
|
186
|
+
`send_email` is a thin convenience wrapper. For template-based sends, double opt-in, preheaders, and other advanced options, use `client.transactionals.create`:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# Send via a saved Template (resolves subject/body/preheader server-side)
|
|
190
|
+
client.transactionals.create(
|
|
191
|
+
to: 'user@example.com',
|
|
192
|
+
template_id: 42,
|
|
193
|
+
reply_to: 'support@yourapp.com',
|
|
194
|
+
include_unsubscribe_link: true
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Override individual fields while still using a template
|
|
198
|
+
client.transactionals.create(
|
|
199
|
+
to: 'user@example.com',
|
|
200
|
+
template_id: 42,
|
|
201
|
+
subject: 'Custom subject for this send'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Set first/last name on a brand-new subscriber created by this send
|
|
205
|
+
client.transactionals.create(
|
|
206
|
+
to: 'new@example.com',
|
|
207
|
+
subject: 'Welcome!',
|
|
208
|
+
body: '<p>Hi {{first_name}}</p>',
|
|
209
|
+
subscriber: { first_name: 'Jane', last_name: 'Doe' }
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Get delivery status (alias of client.get_email)
|
|
213
|
+
client.transactionals.get_transactional(42)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Double Opt-In
|
|
217
|
+
|
|
218
|
+
Pass `double_opt_in: true` to require email confirmation before delivery. The recipient receives a confirmation email; the actual transactional email is held until they confirm. If the recipient is already a confirmed subscriber, `double_opt_in` is ignored and the email sends normally.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Boolean form: uses the channel's default confirmation template
|
|
222
|
+
result = client.transactionals.create(
|
|
223
|
+
to: 'new@example.com',
|
|
224
|
+
subject: 'Welcome!',
|
|
225
|
+
body: '<p>Confirmation email coming first...</p>',
|
|
226
|
+
double_opt_in: true
|
|
227
|
+
)
|
|
228
|
+
result['confirmation_status'] # => 'pending'
|
|
229
|
+
result['confirmation_url'] # => 'https://...'
|
|
230
|
+
|
|
231
|
+
# Hash form: customize the confirmation flow
|
|
232
|
+
client.transactionals.create(
|
|
233
|
+
to: 'new@example.com',
|
|
234
|
+
subject: 'Welcome!',
|
|
235
|
+
body: '<p>...</p>',
|
|
236
|
+
double_opt_in: {
|
|
237
|
+
reply_to: 'support@yourapp.com',
|
|
238
|
+
confirmation_template_id: 7,
|
|
239
|
+
include_unsubscribe_link: true
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Equivalent shortcut for confirmation_template_id at the top level
|
|
244
|
+
client.transactionals.create(
|
|
245
|
+
to: 'new@example.com',
|
|
246
|
+
subject: 'Welcome!',
|
|
247
|
+
body: '<p>...</p>',
|
|
248
|
+
double_opt_in: true,
|
|
249
|
+
confirmation_template_id: 7
|
|
250
|
+
)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
A transactional email server must be configured for the channel and a confirmation template (or default) must exist, otherwise the request returns 422.
|
|
254
|
+
|
|
185
255
|
---
|
|
186
256
|
|
|
187
257
|
## Subscribers
|
|
@@ -263,6 +333,42 @@ client.subscribers.resubscribe('jane@example.com')
|
|
|
263
333
|
client.subscribers.redact('jane@example.com')
|
|
264
334
|
```
|
|
265
335
|
|
|
336
|
+
### Double Opt-In
|
|
337
|
+
|
|
338
|
+
Pass `double_opt_in: true` (or a hash) to create the subscriber in unconfirmed state and queue a confirmation email. If the subscriber already exists and is confirmed, `double_opt_in` is ignored and the existing record is returned.
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Boolean form (uses the channel's default confirmation template)
|
|
342
|
+
result = client.subscribers.create(
|
|
343
|
+
email: 'new@example.com',
|
|
344
|
+
first_name: 'Jane',
|
|
345
|
+
double_opt_in: true
|
|
346
|
+
)
|
|
347
|
+
result['confirmation_status'] # => 'pending'
|
|
348
|
+
result['confirmation_url'] # => '...'
|
|
349
|
+
|
|
350
|
+
# Hash form -- customize reply-to, template, and unsubscribe link
|
|
351
|
+
client.subscribers.create(
|
|
352
|
+
email: 'new@example.com',
|
|
353
|
+
double_opt_in: {
|
|
354
|
+
reply_to: 'support@yourapp.com',
|
|
355
|
+
confirmation_template_id: 7,
|
|
356
|
+
include_unsubscribe_link: true
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# `confirmation_template_id` is also accepted at the top level
|
|
361
|
+
client.subscribers.create(
|
|
362
|
+
email: 'new@example.com',
|
|
363
|
+
double_opt_in: true,
|
|
364
|
+
confirmation_template_id: 7
|
|
365
|
+
)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`double_opt_in` and `confirmation_template_id` are top-level options -- they do **not** go inside the `subscriber:` envelope on the wire (the gem extracts them automatically).
|
|
369
|
+
|
|
370
|
+
The channel must have an active transactional email server and a confirmation template (default or `confirmation_template_id`), otherwise the request returns 422.
|
|
371
|
+
|
|
266
372
|
---
|
|
267
373
|
|
|
268
374
|
## Sequences
|
|
@@ -543,6 +649,201 @@ client.templates.delete(1)
|
|
|
543
649
|
|
|
544
650
|
---
|
|
545
651
|
|
|
652
|
+
## Opt-In Forms
|
|
653
|
+
|
|
654
|
+
Embeddable subscription forms with theming, A/B variants, and analytics.
|
|
655
|
+
|
|
656
|
+
**Required permissions:** `opt_in_forms_read`, `opt_in_forms_write`
|
|
657
|
+
|
|
658
|
+
```ruby
|
|
659
|
+
# List forms (paginated, up to 250 per page; only main forms -- variants are excluded)
|
|
660
|
+
result = client.opt_in_forms.list
|
|
661
|
+
result['opt_in_forms'] # => [{'id' => 1, 'label' => 'Newsletter', ...}, ...]
|
|
662
|
+
result['pagination']['current'] # => 1
|
|
663
|
+
result['pagination']['total'] # => 12
|
|
664
|
+
|
|
665
|
+
# Filter
|
|
666
|
+
client.opt_in_forms.list(filter: 'newsletter', widget_type: 'inline', enabled: 'true')
|
|
667
|
+
|
|
668
|
+
# Get a single form (full payload incl. blocks and settings)
|
|
669
|
+
form = client.opt_in_forms.get_opt_in_form(1)
|
|
670
|
+
|
|
671
|
+
# Create a form
|
|
672
|
+
result = client.opt_in_forms.create(
|
|
673
|
+
label: 'Newsletter Signup',
|
|
674
|
+
form_type: 'inline',
|
|
675
|
+
widget_type: 'inline',
|
|
676
|
+
enabled: true,
|
|
677
|
+
theme_settings: {
|
|
678
|
+
colors: { primary: '#3b82f6', background: '#ffffff', text: '#111827', border: '#d1d5db' }
|
|
679
|
+
},
|
|
680
|
+
automation_settings: {
|
|
681
|
+
tag_list: 'newsletter',
|
|
682
|
+
send_welcome_email: true,
|
|
683
|
+
double_opt_in: true,
|
|
684
|
+
sequence_ids: [3]
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# Update (deeply nested settings hashes pass through verbatim)
|
|
689
|
+
client.opt_in_forms.update(1, enabled: false)
|
|
690
|
+
|
|
691
|
+
# Delete
|
|
692
|
+
client.opt_in_forms.delete(1)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Analytics
|
|
696
|
+
|
|
697
|
+
```ruby
|
|
698
|
+
# Last 30 days by default
|
|
699
|
+
client.opt_in_forms.analytics(1)
|
|
700
|
+
|
|
701
|
+
# Custom date range -- accepts Date, Time, or ISO-8601 strings
|
|
702
|
+
client.opt_in_forms.analytics(1,
|
|
703
|
+
start_date: Date.new(2026, 1, 1),
|
|
704
|
+
end_date: Date.new(2026, 1, 31)
|
|
705
|
+
)
|
|
706
|
+
# Returns: { totals: { views, unique_views, submissions, conversion_rate },
|
|
707
|
+
# daily: [...], variants: [...] }
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### A/B Variants
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
# Create a variant of a form (defaults to "Variant N" / weight 50)
|
|
714
|
+
client.opt_in_forms.create_variant(1, name: 'B', weight: 50)
|
|
715
|
+
|
|
716
|
+
# Duplicate a form into a new top-level form (counts toward your plan limit)
|
|
717
|
+
client.opt_in_forms.duplicate(1, label: 'Newsletter Signup (Copy)')
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
> **Note:** `index`/`show` and `create`/`update` return slightly different JSON shapes (the index/show responses go through JBuilder views; create/update use a richer inline serializer with analytics counts and embed URL). Don't depend on field-level parity between these paths.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Email Servers
|
|
725
|
+
|
|
726
|
+
Configure outbound email providers for a channel (SMTP, AWS SES, Postmark, Inboxroad, SMTP.com, etc.).
|
|
727
|
+
|
|
728
|
+
**Required permissions:** `email_servers_read`, `email_servers_write`
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
# List email servers
|
|
732
|
+
result = client.email_servers.list
|
|
733
|
+
result['data'] # => [{'id' => 1, 'label' => 'Primary SES', 'vendor' => 'aws_ses', ...}, ...]
|
|
734
|
+
result['total'] # => 3
|
|
735
|
+
|
|
736
|
+
# Pagination
|
|
737
|
+
client.email_servers.list(limit: 10, offset: 0)
|
|
738
|
+
|
|
739
|
+
# Get a single email server
|
|
740
|
+
es = client.email_servers.get_email_server(1)
|
|
741
|
+
|
|
742
|
+
# Create -- example: AWS SES
|
|
743
|
+
client.email_servers.create(
|
|
744
|
+
label: 'Primary SES',
|
|
745
|
+
vendor: 'aws_ses',
|
|
746
|
+
delivery_method: 'aws_ses',
|
|
747
|
+
active: true,
|
|
748
|
+
aws_region: 'us-east-1',
|
|
749
|
+
aws_access_key_id: 'AKIA...',
|
|
750
|
+
aws_secret_access_key: 'secret...',
|
|
751
|
+
use_for_broadcasts: true,
|
|
752
|
+
use_for_sequences: true,
|
|
753
|
+
use_for_transactionals: true
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Create -- example: SMTP
|
|
757
|
+
client.email_servers.create(
|
|
758
|
+
label: 'Backup SMTP',
|
|
759
|
+
vendor: 'smtp',
|
|
760
|
+
delivery_method: 'smtp',
|
|
761
|
+
smtp_address: 'smtp.example.com',
|
|
762
|
+
smtp_port: 587,
|
|
763
|
+
smtp_username: 'user',
|
|
764
|
+
smtp_password: 'pass',
|
|
765
|
+
smtp_authentication: 'plain',
|
|
766
|
+
smtp_enable_starttls_auto: true,
|
|
767
|
+
emails_per_hour: 10000
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Update -- pass only the fields you want to change
|
|
771
|
+
client.email_servers.update(1, label: 'Primary SES (updated)', emails_per_hour: 50000)
|
|
772
|
+
|
|
773
|
+
# Test the connection (toggles `active` based on result)
|
|
774
|
+
result = client.email_servers.test_connection(1)
|
|
775
|
+
result['success'] # => true / false
|
|
776
|
+
result['message'] # => 'Connection successful' / 'Connection failed'
|
|
777
|
+
|
|
778
|
+
# Delete
|
|
779
|
+
client.email_servers.delete(1)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Credential Redaction
|
|
783
|
+
|
|
784
|
+
API responses redact credential fields with bullet characters (e.g. `smtp_password: "abcd••••••wxyz"`). **Never round-trip a fetched response back into `update`** -- the gem detects redacted-shape values on known credential fields (`smtp_password`, `aws_*`, `postmark_api_token`, `inboxroad_api_token`, `smtp_com_api_key`) and silently strips them from the payload (with a warning) so they don't overwrite the real value:
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
# Safe -- only the fields you actually want to change
|
|
788
|
+
client.email_servers.update(1, label: 'New label', emails_per_hour: 25000)
|
|
789
|
+
|
|
790
|
+
# This would corrupt credentials WITHOUT the gem's scrubber:
|
|
791
|
+
es = client.email_servers.get_email_server(1)
|
|
792
|
+
client.email_servers.update(1, **es) # bullets get scrubbed, label still updates
|
|
793
|
+
|
|
794
|
+
# To rotate a credential, pass the real new value
|
|
795
|
+
client.email_servers.update(1, smtp_password: 'new-real-secret')
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Cross-Channel Copy (admin tokens only)
|
|
799
|
+
|
|
800
|
+
`copy_to_channel` clones an email server (with all settings and headers) into another channel. **Requires an admin/system token** with `email_servers_write` permission. Regular per-channel tokens get `Broadcast::AuthorizationError`. In SaaS mode, the target channel must be in the admin token creator's account.
|
|
801
|
+
|
|
802
|
+
```ruby
|
|
803
|
+
admin_client = Broadcast::Client.new(
|
|
804
|
+
api_token: ENV['BROADCAST_ADMIN_TOKEN'],
|
|
805
|
+
broadcast_channel_id: 1 # the source server's channel
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
admin_client.email_servers.copy_to_channel(99, target_channel_id: 7)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## Channel Scoping (Admin/System Tokens)
|
|
814
|
+
|
|
815
|
+
Regular API tokens are scoped to a single broadcast channel automatically. Admin/system tokens are not -- they require `broadcast_channel_id` on every request to indicate which channel they're acting on.
|
|
816
|
+
|
|
817
|
+
The gem auto-includes `broadcast_channel_id` from your `Configuration` on every request:
|
|
818
|
+
|
|
819
|
+
```ruby
|
|
820
|
+
# Set globally
|
|
821
|
+
client = Broadcast::Client.new(
|
|
822
|
+
api_token: ENV['BROADCAST_ADMIN_TOKEN'],
|
|
823
|
+
broadcast_channel_id: 1
|
|
824
|
+
)
|
|
825
|
+
client.email_servers.list # broadcast_channel_id=1 is appended automatically
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
For multi-channel scripts, use `with_channel` to scope a block of calls to a specific channel:
|
|
829
|
+
|
|
830
|
+
```ruby
|
|
831
|
+
client = Broadcast::Client.new(api_token: ENV['BROADCAST_ADMIN_TOKEN'])
|
|
832
|
+
|
|
833
|
+
client.with_channel(1) do
|
|
834
|
+
client.email_servers.list
|
|
835
|
+
client.opt_in_forms.list
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
client.with_channel(2) do
|
|
839
|
+
client.subscribers.list
|
|
840
|
+
end
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
`with_channel` overrides the config-level value inside the block. Per-call `broadcast_channel_id:` always wins over both. The override is thread-local, so it's safe to share a `Client` instance across threads.
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
546
847
|
## Webhook Endpoints
|
|
547
848
|
|
|
548
849
|
Receive real-time notifications when events occur (email delivered, subscriber created, sequence completed, etc.).
|
|
@@ -640,6 +941,7 @@ All API errors inherit from `Broadcast::Error`. Put specific errors before gener
|
|
|
640
941
|
begin
|
|
641
942
|
client.send_email(to: 'user@example.com', subject: 'Hi', body: 'Hello')
|
|
642
943
|
rescue Broadcast::AuthenticationError # 401 -- invalid or expired API token
|
|
944
|
+
rescue Broadcast::AuthorizationError # 403 -- token lacks the required permission, or admin-only endpoint
|
|
643
945
|
rescue Broadcast::NotFoundError # 404 -- resource does not exist
|
|
644
946
|
rescue Broadcast::ValidationError # 422 -- missing or invalid parameters
|
|
645
947
|
rescue Broadcast::RateLimitError # 429 -- exceeded 120 requests/minute
|
|
@@ -665,6 +967,8 @@ Each token can be scoped to specific resources. The ActionMailer delivery method
|
|
|
665
967
|
| Broadcasts | `broadcasts_read` -- list, get, statistics | `broadcasts_write` -- create, update, delete, send, schedule |
|
|
666
968
|
| Segments | `segments_read` -- list, get | `segments_write` -- create, update, delete |
|
|
667
969
|
| Templates | `templates_read` -- list, get | `templates_write` -- create, update, delete |
|
|
970
|
+
| Opt-In Forms | `opt_in_forms_read` -- list, get, analytics | `opt_in_forms_write` -- create, update, delete, create_variant, duplicate |
|
|
971
|
+
| Email Servers | `email_servers_read` -- list, get | `email_servers_write` -- create, update, delete, test_connection, copy_to_channel (admin) |
|
|
668
972
|
| Webhook Endpoints | `webhook_endpoints_read` -- list, get, deliveries | `webhook_endpoints_write` -- create, update, delete, test |
|
|
669
973
|
|
|
670
974
|
---
|
|
@@ -677,6 +981,12 @@ Each token can be scoped to specific resources. The ActionMailer delivery method
|
|
|
677
981
|
- **Wrong host:** If you're self-hosting, make sure `host` points to your Broadcast instance, not `sendbroadcast.com`.
|
|
678
982
|
- **Missing permissions:** Your token may not have the required permissions for the resource you're accessing. Check the [permissions table](#api-token-permissions).
|
|
679
983
|
|
|
984
|
+
### `Broadcast::AuthorizationError` (403)
|
|
985
|
+
|
|
986
|
+
- **Missing permission:** Your token doesn't have the read or write permission for the resource (e.g. calling `client.opt_in_forms.create` with a token that only has `opt_in_forms_read`).
|
|
987
|
+
- **Admin-only endpoint:** `email_servers.copy_to_channel` requires an admin/system token. Regular per-channel tokens cannot perform cross-channel operations.
|
|
988
|
+
- **Missing channel scope on admin token:** Admin tokens require `broadcast_channel_id` on every request. Set it on `Broadcast::Client.new(broadcast_channel_id: …)` or wrap calls with `client.with_channel(id) { … }`.
|
|
989
|
+
|
|
680
990
|
### `Broadcast::ValidationError` (422)
|
|
681
991
|
|
|
682
992
|
- **Missing required fields:** Check the parameters table for the method you're calling.
|
data/lib/broadcast/client.rb
CHANGED
|
@@ -6,6 +6,8 @@ require 'uri'
|
|
|
6
6
|
|
|
7
7
|
module Broadcast
|
|
8
8
|
class Client
|
|
9
|
+
CHANNEL_OVERRIDE_KEY = :__broadcast_ruby_channel_override
|
|
10
|
+
|
|
9
11
|
attr_reader :config
|
|
10
12
|
|
|
11
13
|
def initialize(**settings)
|
|
@@ -14,16 +16,35 @@ module Broadcast
|
|
|
14
16
|
@config.validate!
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
# ---
|
|
19
|
+
# --- Channel scoping (admin/system tokens) ---
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Run a block with a temporary broadcast_channel_id override that will be
|
|
22
|
+
# auto-included on every request inside the block. Useful for admin/system
|
|
23
|
+
# tokens that need to scope each call to a specific channel.
|
|
24
|
+
#
|
|
25
|
+
# client.with_channel(123) do
|
|
26
|
+
# client.email_servers.list
|
|
27
|
+
# end
|
|
28
|
+
def with_channel(broadcast_channel_id)
|
|
29
|
+
key = channel_override_key
|
|
30
|
+
previous = Thread.current[key]
|
|
31
|
+
Thread.current[key] = broadcast_channel_id
|
|
32
|
+
yield self
|
|
33
|
+
ensure
|
|
34
|
+
Thread.current[key] = previous
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# --- Transactional email (convenience shims) ---
|
|
38
|
+
|
|
39
|
+
# Thin convenience wrapper around `transactionals.create`. Use
|
|
40
|
+
# `client.transactionals.create` directly for template_id, double_opt_in,
|
|
41
|
+
# preheader, and other advanced options.
|
|
42
|
+
def send_email(to:, subject: nil, body: nil, reply_to: nil)
|
|
43
|
+
transactionals.create(to: to, subject: subject, body: body, reply_to: reply_to)
|
|
23
44
|
end
|
|
24
45
|
|
|
25
46
|
def get_email(id)
|
|
26
|
-
|
|
47
|
+
transactionals.get_transactional(id)
|
|
27
48
|
end
|
|
28
49
|
|
|
29
50
|
# --- Resource sub-clients ---
|
|
@@ -52,35 +73,76 @@ module Broadcast
|
|
|
52
73
|
@webhook_endpoints ||= Resources::WebhookEndpoints.new(self)
|
|
53
74
|
end
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if method == :get && body_or_params.is_a?(Hash) && body_or_params.any?
|
|
60
|
-
uri.query = URI.encode_www_form(flatten_params(body_or_params))
|
|
61
|
-
end
|
|
76
|
+
def transactionals
|
|
77
|
+
@transactionals ||= Resources::Transactionals.new(self)
|
|
78
|
+
end
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
http.open_timeout = @config.open_timeout
|
|
67
|
-
http.read_timeout = @config.timeout
|
|
80
|
+
def opt_in_forms
|
|
81
|
+
@opt_in_forms ||= Resources::OptInForms.new(self)
|
|
82
|
+
end
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
def email_servers
|
|
85
|
+
@email_servers ||= Resources::EmailServers.new(self)
|
|
86
|
+
end
|
|
71
87
|
|
|
72
|
-
|
|
88
|
+
# @api private
|
|
89
|
+
def request(method, path, body_or_params = nil)
|
|
90
|
+
payload = inject_channel_scope(body_or_params)
|
|
91
|
+
uri = build_uri(path, method, payload)
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
log_response(response) if @config.debug
|
|
76
|
-
handle_response(response)
|
|
77
|
-
end
|
|
93
|
+
retry_with_backoff { execute(method, uri, payload) }
|
|
78
94
|
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
79
95
|
raise Broadcast::TimeoutError, "Request timeout: #{e.message}"
|
|
80
96
|
end
|
|
81
97
|
|
|
82
98
|
private
|
|
83
99
|
|
|
100
|
+
def channel_override_key
|
|
101
|
+
:"#{CHANNEL_OVERRIDE_KEY}_#{object_id}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def active_channel_id
|
|
105
|
+
Thread.current[channel_override_key] || @config.broadcast_channel_id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Auto-include broadcast_channel_id in request payload when configured (or
|
|
109
|
+
# set via with_channel) and not already specified by the caller.
|
|
110
|
+
def inject_channel_scope(body_or_params)
|
|
111
|
+
channel_id = active_channel_id
|
|
112
|
+
return body_or_params if channel_id.nil?
|
|
113
|
+
|
|
114
|
+
payload = body_or_params.is_a?(Hash) ? body_or_params.dup : {}
|
|
115
|
+
return payload if payload[:broadcast_channel_id] || payload['broadcast_channel_id']
|
|
116
|
+
|
|
117
|
+
payload[:broadcast_channel_id] = channel_id
|
|
118
|
+
payload
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_uri(path, method, payload)
|
|
122
|
+
uri = URI("#{@config.host}#{path}")
|
|
123
|
+
uri.query = URI.encode_www_form(flatten_params(payload)) if method == :get && payload_present?(payload)
|
|
124
|
+
uri
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def execute(method, uri, payload)
|
|
128
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
129
|
+
http.use_ssl = uri.scheme == 'https'
|
|
130
|
+
http.open_timeout = @config.open_timeout
|
|
131
|
+
http.read_timeout = @config.timeout
|
|
132
|
+
|
|
133
|
+
req = build_request(method, uri)
|
|
134
|
+
req.body = payload.to_json if method != :get && payload_present?(payload)
|
|
135
|
+
|
|
136
|
+
log_request(req, method == :get ? nil : payload) if @config.debug
|
|
137
|
+
response = http.request(req)
|
|
138
|
+
log_response(response) if @config.debug
|
|
139
|
+
handle_response(response)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def payload_present?(payload)
|
|
143
|
+
payload.is_a?(Hash) && payload.any?
|
|
144
|
+
end
|
|
145
|
+
|
|
84
146
|
def build_request(method, uri)
|
|
85
147
|
klass = case method
|
|
86
148
|
when :get then Net::HTTP::Get
|
|
@@ -97,25 +159,34 @@ module Broadcast
|
|
|
97
159
|
req
|
|
98
160
|
end
|
|
99
161
|
|
|
162
|
+
ERROR_MAPPING = {
|
|
163
|
+
401 => [AuthenticationError, 'Authentication failed'],
|
|
164
|
+
403 => [AuthorizationError, 'Not authorized'],
|
|
165
|
+
404 => [NotFoundError, 'Resource not found'],
|
|
166
|
+
422 => [ValidationError, 'Validation failed'],
|
|
167
|
+
429 => [RateLimitError, 'Rate limit exceeded']
|
|
168
|
+
}.freeze
|
|
169
|
+
SERVER_ERROR_CODES = [500, 502, 503, 504].freeze
|
|
170
|
+
private_constant :ERROR_MAPPING, :SERVER_ERROR_CODES
|
|
171
|
+
|
|
100
172
|
def handle_response(response)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
raise AuthenticationError, parse_error(response) || 'Authentication failed'
|
|
108
|
-
when 404
|
|
109
|
-
raise NotFoundError, parse_error(response) || 'Resource not found'
|
|
110
|
-
when 422
|
|
111
|
-
raise ValidationError, parse_error(response) || 'Validation failed'
|
|
112
|
-
when 429
|
|
113
|
-
raise RateLimitError, parse_error(response) || 'Rate limit exceeded'
|
|
114
|
-
when 500, 502, 503, 504
|
|
115
|
-
raise APIError, parse_error(response) || "Server error (#{response.code})"
|
|
116
|
-
else
|
|
117
|
-
raise APIError, parse_error(response) || "Unexpected response: #{response.code}"
|
|
173
|
+
code = response.code.to_i
|
|
174
|
+
return parse_success_body(response) if [200, 201].include?(code)
|
|
175
|
+
|
|
176
|
+
if (mapping = ERROR_MAPPING[code])
|
|
177
|
+
klass, default = mapping
|
|
178
|
+
raise klass, parse_error(response) || default
|
|
118
179
|
end
|
|
180
|
+
|
|
181
|
+
raise APIError, parse_error(response) || "Server error (#{code})" if SERVER_ERROR_CODES.include?(code)
|
|
182
|
+
|
|
183
|
+
raise APIError, parse_error(response) || "Unexpected response: #{code}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse_success_body(response)
|
|
187
|
+
return {} if response.body.nil? || response.body.strip.empty?
|
|
188
|
+
|
|
189
|
+
JSON.parse(response.body)
|
|
119
190
|
end
|
|
120
191
|
|
|
121
192
|
def parse_error(response)
|
|
@@ -9,7 +9,8 @@ module Broadcast
|
|
|
9
9
|
:retry_attempts,
|
|
10
10
|
:retry_delay,
|
|
11
11
|
:logger,
|
|
12
|
-
:debug
|
|
12
|
+
:debug,
|
|
13
|
+
:broadcast_channel_id
|
|
13
14
|
|
|
14
15
|
def initialize
|
|
15
16
|
@api_token = nil
|
|
@@ -20,6 +21,7 @@ module Broadcast
|
|
|
20
21
|
@retry_delay = 1
|
|
21
22
|
@logger = nil
|
|
22
23
|
@debug = false
|
|
24
|
+
@broadcast_channel_id = nil
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def validate!
|
data/lib/broadcast/errors.rb
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class EmailServers < Base
|
|
6
|
+
# Fields the API returns redacted (bullet-masked). When updating, never
|
|
7
|
+
# round-trip these values back from a fetch — the gem strips them out
|
|
8
|
+
# of the payload (with a logger warning) to prevent corrupting credentials.
|
|
9
|
+
REDACTED_FIELDS = %i[
|
|
10
|
+
smtp_password
|
|
11
|
+
aws_access_key_id
|
|
12
|
+
aws_secret_access_key
|
|
13
|
+
outbound_aws_access_key_id
|
|
14
|
+
outbound_aws_secret_access_key
|
|
15
|
+
postmark_api_token
|
|
16
|
+
inboxroad_api_token
|
|
17
|
+
smtp_com_api_key
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Matches the API's redaction shape: 8 bullets, OR 4-char prefix + bullets + 4-char suffix.
|
|
21
|
+
REDACTED_PATTERN = /\A(?:•{8}|.{0,4}•+.{0,4})\z/
|
|
22
|
+
|
|
23
|
+
def list(limit: nil, offset: nil)
|
|
24
|
+
params = {}
|
|
25
|
+
params[:limit] = limit unless limit.nil?
|
|
26
|
+
params[:offset] = offset unless offset.nil?
|
|
27
|
+
get('/api/v1/email_servers', params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_email_server(id)
|
|
31
|
+
get("/api/v1/email_servers/#{id}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create(**attrs)
|
|
35
|
+
post('/api/v1/email_servers', { email_server: attrs })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Update an email server. Attrs are wrapped under `email_server:` on the wire.
|
|
39
|
+
#
|
|
40
|
+
# CAUTION: API responses redact credential fields (e.g. `smtp_password`)
|
|
41
|
+
# with bullet characters. Never echo a fetched response back into update —
|
|
42
|
+
# this method scrubs values that match the redaction pattern, but you
|
|
43
|
+
# should pass only the fields you actually want to change.
|
|
44
|
+
def update(id, **attrs)
|
|
45
|
+
scrubbed = scrub_redacted(attrs)
|
|
46
|
+
patch("/api/v1/email_servers/#{id}", { email_server: scrubbed })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete(id)
|
|
50
|
+
@client.request(:delete, "/api/v1/email_servers/#{id}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_connection(id)
|
|
54
|
+
post("/api/v1/email_servers/#{id}/test_connection")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Copy an email server to another channel. Requires an admin/system token.
|
|
58
|
+
# In SaaS mode, target_channel_id is scoped to the token creator's account.
|
|
59
|
+
def copy_to_channel(id, target_channel_id:)
|
|
60
|
+
post("/api/v1/email_servers/#{id}/copy_to_channel", { target_channel_id: target_channel_id })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def scrub_redacted(attrs)
|
|
66
|
+
scrubbed = {}
|
|
67
|
+
attrs.each do |key, value|
|
|
68
|
+
if REDACTED_FIELDS.include?(key.to_sym) && value.is_a?(String) && value.match?(REDACTED_PATTERN)
|
|
69
|
+
warn_redacted(key)
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
scrubbed[key] = value
|
|
73
|
+
end
|
|
74
|
+
scrubbed
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def warn_redacted(field)
|
|
78
|
+
msg = "[broadcast-ruby] Dropped redacted #{field} from update payload — " \
|
|
79
|
+
'pass the real credential or omit the field'
|
|
80
|
+
if @client.config.logger
|
|
81
|
+
@client.config.logger.warn(msg)
|
|
82
|
+
else
|
|
83
|
+
Kernel.warn(msg)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Broadcast
|
|
6
|
+
module Resources
|
|
7
|
+
class OptInForms < Base
|
|
8
|
+
# List opt-in forms.
|
|
9
|
+
#
|
|
10
|
+
# NOTE: returns up to 250 results per page along with `pagination`
|
|
11
|
+
# metadata. Variants are excluded (only main forms are returned).
|
|
12
|
+
# Pass `page:` to advance.
|
|
13
|
+
#
|
|
14
|
+
# Optional filters: filter: (label substring), widget_type:, enabled: 'true'
|
|
15
|
+
def list(**params)
|
|
16
|
+
get('/api/v1/opt_in_forms', params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_opt_in_form(id)
|
|
20
|
+
get("/api/v1/opt_in_forms/#{id}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create an opt-in form. Attrs are wrapped under `opt_in_form:` on the wire.
|
|
24
|
+
# Nested settings hashes (theme_settings, automation_settings, security_settings,
|
|
25
|
+
# trigger_settings, widget_settings) and arrays (opt_in_form_blocks_attributes,
|
|
26
|
+
# opt_in_post_submission_blocks_attributes) are passed through verbatim.
|
|
27
|
+
def create(**attrs)
|
|
28
|
+
post('/api/v1/opt_in_forms', { opt_in_form: attrs })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update(id, **attrs)
|
|
32
|
+
patch("/api/v1/opt_in_forms/#{id}", { opt_in_form: attrs })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(id)
|
|
36
|
+
@client.request(:delete, "/api/v1/opt_in_forms/#{id}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Performance analytics for the form. start_date/end_date accept Date,
|
|
40
|
+
# Time, or ISO-8601 strings (default: last 30 days).
|
|
41
|
+
def analytics(id, start_date: nil, end_date: nil)
|
|
42
|
+
params = {}
|
|
43
|
+
params[:start_date] = coerce_date(start_date) if start_date
|
|
44
|
+
params[:end_date] = coerce_date(end_date) if end_date
|
|
45
|
+
get("/api/v1/opt_in_forms/#{id}/analytics", params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_variant(id, name: nil, weight: nil)
|
|
49
|
+
body = {}
|
|
50
|
+
body[:name] = name unless name.nil?
|
|
51
|
+
body[:weight] = weight unless weight.nil?
|
|
52
|
+
post("/api/v1/opt_in_forms/#{id}/variants", body)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def duplicate(id, label: nil)
|
|
56
|
+
body = {}
|
|
57
|
+
body[:label] = label unless label.nil?
|
|
58
|
+
post("/api/v1/opt_in_forms/#{id}/duplicate", body)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def coerce_date(value)
|
|
64
|
+
case value
|
|
65
|
+
when Date, Time, DateTime then value.iso8601
|
|
66
|
+
else value.to_s
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -11,8 +11,26 @@ module Broadcast
|
|
|
11
11
|
get('/api/v1/subscribers/find.json', { email: email })
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Create or upsert a subscriber.
|
|
15
|
+
#
|
|
16
|
+
# Subscriber attributes (wrapped under `subscriber:` on the wire):
|
|
17
|
+
# email:, first_name:, last_name:, is_active:, source:,
|
|
18
|
+
# subscribed_at:, ip_address:, tags: [...], custom_data: {...}
|
|
19
|
+
#
|
|
20
|
+
# Top-level options (NOT wrapped under `subscriber:`):
|
|
21
|
+
# double_opt_in: true | { reply_to:, confirmation_template_id:, include_unsubscribe_link: }
|
|
22
|
+
# When set, the subscriber is created in unconfirmed state
|
|
23
|
+
# and a confirmation email is queued.
|
|
24
|
+
# confirmation_template_id: custom confirmation template (used with double_opt_in: true)
|
|
14
25
|
def create(**attrs)
|
|
15
|
-
|
|
26
|
+
double_opt_in = attrs.delete(:double_opt_in)
|
|
27
|
+
confirmation_template_id = attrs.delete(:confirmation_template_id)
|
|
28
|
+
|
|
29
|
+
payload = { subscriber: attrs }
|
|
30
|
+
payload[:double_opt_in] = double_opt_in unless double_opt_in.nil?
|
|
31
|
+
payload[:confirmation_template_id] = confirmation_template_id unless confirmation_template_id.nil?
|
|
32
|
+
|
|
33
|
+
post('/api/v1/subscribers.json', payload)
|
|
16
34
|
end
|
|
17
35
|
|
|
18
36
|
def update(email, **attrs)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Broadcast
|
|
4
|
+
module Resources
|
|
5
|
+
class Transactionals < Base
|
|
6
|
+
# Send a transactional email.
|
|
7
|
+
#
|
|
8
|
+
# Required:
|
|
9
|
+
# to: recipient email address
|
|
10
|
+
#
|
|
11
|
+
# One of subject/body or template_id is required (template_id resolves
|
|
12
|
+
# subject and body server-side; subject/body override the template).
|
|
13
|
+
#
|
|
14
|
+
# Optional:
|
|
15
|
+
# subject:, body:, preheader:
|
|
16
|
+
# reply_to:
|
|
17
|
+
# template_id: resolve subject/body/preheader from a Template
|
|
18
|
+
# include_unsubscribe_link: boolean
|
|
19
|
+
# double_opt_in: true | { reply_to:, confirmation_template_id:, include_unsubscribe_link: }
|
|
20
|
+
# Holds the email until the recipient confirms.
|
|
21
|
+
# confirmation_template_id: custom confirmation template (used with double_opt_in: true)
|
|
22
|
+
# subscriber: { first_name:, last_name: } — populates Subscriber on first send
|
|
23
|
+
# rubocop:disable Metrics/ParameterLists -- mirrors the API's flat param surface
|
|
24
|
+
def create(to:, subject: nil, body: nil, reply_to: nil, preheader: nil,
|
|
25
|
+
template_id: nil, include_unsubscribe_link: nil,
|
|
26
|
+
double_opt_in: nil, confirmation_template_id: nil,
|
|
27
|
+
subscriber: nil, **extra)
|
|
28
|
+
# rubocop:enable Metrics/ParameterLists
|
|
29
|
+
payload = { to: to }
|
|
30
|
+
payload[:subject] = subject unless subject.nil?
|
|
31
|
+
payload[:body] = body unless body.nil?
|
|
32
|
+
payload[:preheader] = preheader unless preheader.nil?
|
|
33
|
+
payload[:reply_to] = reply_to unless reply_to.nil?
|
|
34
|
+
payload[:template_id] = template_id unless template_id.nil?
|
|
35
|
+
payload[:include_unsubscribe_link] = include_unsubscribe_link unless include_unsubscribe_link.nil?
|
|
36
|
+
payload[:double_opt_in] = double_opt_in unless double_opt_in.nil?
|
|
37
|
+
payload[:confirmation_template_id] = confirmation_template_id unless confirmation_template_id.nil?
|
|
38
|
+
payload[:subscriber] = subscriber unless subscriber.nil?
|
|
39
|
+
payload.merge!(extra) unless extra.empty?
|
|
40
|
+
|
|
41
|
+
post('/api/v1/transactionals.json', payload)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_transactional(id)
|
|
45
|
+
get("/api/v1/transactionals/#{id}.json")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/broadcast/version.rb
CHANGED
data/lib/broadcast.rb
CHANGED
|
@@ -12,6 +12,9 @@ require_relative 'broadcast/resources/broadcasts'
|
|
|
12
12
|
require_relative 'broadcast/resources/segments'
|
|
13
13
|
require_relative 'broadcast/resources/templates'
|
|
14
14
|
require_relative 'broadcast/resources/webhook_endpoints'
|
|
15
|
+
require_relative 'broadcast/resources/transactionals'
|
|
16
|
+
require_relative 'broadcast/resources/opt_in_forms'
|
|
17
|
+
require_relative 'broadcast/resources/email_servers'
|
|
15
18
|
|
|
16
19
|
# ActionMailer integration — only loaded when Rails is present
|
|
17
20
|
if defined?(Rails::Railtie)
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Chiu
|
|
@@ -47,10 +47,13 @@ files:
|
|
|
47
47
|
- lib/broadcast/railtie.rb
|
|
48
48
|
- lib/broadcast/resources/base.rb
|
|
49
49
|
- lib/broadcast/resources/broadcasts.rb
|
|
50
|
+
- lib/broadcast/resources/email_servers.rb
|
|
51
|
+
- lib/broadcast/resources/opt_in_forms.rb
|
|
50
52
|
- lib/broadcast/resources/segments.rb
|
|
51
53
|
- lib/broadcast/resources/sequences.rb
|
|
52
54
|
- lib/broadcast/resources/subscribers.rb
|
|
53
55
|
- lib/broadcast/resources/templates.rb
|
|
56
|
+
- lib/broadcast/resources/transactionals.rb
|
|
54
57
|
- lib/broadcast/resources/webhook_endpoints.rb
|
|
55
58
|
- lib/broadcast/version.rb
|
|
56
59
|
- lib/broadcast/webhook.rb
|