letmesendemail 0.1.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/docs/docs.md ADDED
@@ -0,0 +1,513 @@
1
+ # letmesend.email SDK for Ruby
2
+
3
+ ## Overview
4
+
5
+ The `letmesendemail` gem is the official Ruby client for the letmesend.email API. It provides resource-oriented email, domain, contact, contact-category, and email-topic operations; structured errors; cursor pagination; conservative retries; and webhook verification.
6
+
7
+ API responses are returned as `LetMeSendEmail::Models::Model` values with hash-style access and recursive serialization.
8
+
9
+ ## Requirements
10
+
11
+ - Ruby 3.1 or newer
12
+ - Bundler or RubyGems
13
+ - A letmesend.email API key
14
+
15
+ The SDK uses Ruby's `net/http` and `json` libraries. Its only declared runtime gem is `base64`.
16
+
17
+ ## Installation
18
+
19
+ With Bundler:
20
+
21
+ ```bash
22
+ bundle add letmesendemail
23
+ ```
24
+
25
+ Or install the gem directly:
26
+
27
+ ```bash
28
+ gem install letmesendemail
29
+ ```
30
+
31
+ Load the package with:
32
+
33
+ ```ruby
34
+ require "letmesendemail"
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ Create an API key in your letmesend.email account and expose it to the application as `LETMESENDEMAIL_API_KEY`. Do not commit API keys or place them in source code.
40
+
41
+ ```ruby
42
+ api_key = ENV.fetch("LETMESENDEMAIL_API_KEY")
43
+ raise "LETMESENDEMAIL_API_KEY must not be empty" if api_key.empty?
44
+
45
+ client = LetMeSendEmail::Client.new(api_key: api_key)
46
+ ```
47
+
48
+ The SDK sends the key as a Bearer token. It rejects empty keys, surrounding whitespace, and control characters.
49
+
50
+ ## Quick Start
51
+
52
+ ```ruby
53
+ require "letmesendemail"
54
+
55
+ api_key = ENV.fetch("LETMESENDEMAIL_API_KEY")
56
+ raise "LETMESENDEMAIL_API_KEY must not be empty" if api_key.empty?
57
+
58
+ client = LetMeSendEmail::Client.new(api_key: api_key)
59
+
60
+ begin
61
+ result = client.emails.send(
62
+ from: "Acme <hello@example.com>",
63
+ to: ["customer@example.net"],
64
+ subject: "Welcome",
65
+ html: "<p>Thanks for joining.</p>",
66
+ text: "Thanks for joining."
67
+ )
68
+ puts "Accepted email #{result['id']} with status #{result['status']}"
69
+ rescue LetMeSendEmail::Error => e
70
+ warn "Email request failed: #{e.message}"
71
+ end
72
+ ```
73
+
74
+ ## Client Configuration
75
+
76
+ For custom settings, create a configuration object before creating the client:
77
+
78
+ ```ruby
79
+ config = LetMeSendEmail::Config.new(api_key)
80
+ config.base_url = "https://letmesend.email/api/v1"
81
+ config.timeout_ms = 30_000
82
+ config.retries = 2
83
+
84
+ client = LetMeSendEmail::Client.new(config: config)
85
+ ```
86
+
87
+ | Option | Default | Behavior |
88
+ |---|---:|---|
89
+ | `api_key` | Required | Bearer token; must be a non-empty safe string. |
90
+ | `base_url` | `https://letmesend.email/api/v1` | Absolute HTTP or HTTPS URL without credentials, query, or fragment. Trailing slashes are removed. |
91
+ | `timeout_ms` | `30000` | Positive timeout in milliseconds applied to connection, read, and write phases. |
92
+ | `retries` | `0` | Additional attempts, from 0 through 20. |
93
+
94
+ Requests use `letmesendemail-ruby/<version>` as their User-Agent. Supplying both `api_key:` and `config:` is rejected.
95
+ The client takes an immutable snapshot of the validated configuration when it is created; later changes to the original configuration object do not affect that client.
96
+
97
+ ### Retry behavior
98
+
99
+ When retries are enabled, GET and DELETE requests may retry. POST and PUT requests require a non-empty `Idempotency-Key`; email and domain verification calls do not retry. The SDK retries network failures, timeouts, HTTP 408, 500, 502, 503, and 504. HTTP 429 retries only when `Retry-After` is a positive delta-seconds value or future HTTP date no more than 300 seconds away.
100
+
101
+ Ordinary retry delay starts at 100 milliseconds, doubles per retry, adds random jitter from zero through 25% of that delay, and caps at 300 seconds. A valid `Retry-After` delay is used exactly without jitter.
102
+
103
+ ## Emails
104
+
105
+ Email operations are available through `client.emails`. Every method returns a `LetMeSendEmail::Models::Model`.
106
+
107
+ ### Send an Email
108
+
109
+ ```ruby
110
+ result = client.emails.send(
111
+ from: "Acme <hello@example.com>",
112
+ to: ["customer@example.net"],
113
+ subject: "Your receipt",
114
+ html: "<p>Your receipt is ready.</p>",
115
+ text: "Your receipt is ready.",
116
+ type: "transactional",
117
+ event_name: "receipt.created",
118
+ email_topic_id: "topic_123",
119
+ reply_to: ["support@example.com"],
120
+ cc: ["accounts@example.com"],
121
+ bcc: ["archive@example.com"],
122
+ headers: { "X-Order-ID" => "order_123" },
123
+ idempotency_key: "receipt-order-123"
124
+ )
125
+
126
+ puts result["id"]
127
+ puts result["status"]
128
+ puts result["emails"].inspect
129
+ puts result["restricted_emails"].inspect
130
+ ```
131
+
132
+ `from`, `to`, and `subject` are required. Provide at least one of `html` or `text` as required by your email workflow. `type` supports `broadcast` and `transactional`. Address arrays accept plain addresses or display-name forms such as `Taylor <customer@example.net>`.
133
+
134
+ ### Send with a Template
135
+
136
+ ```ruby
137
+ result = client.emails.send_with_template(
138
+ from: "Acme <hello@example.com>",
139
+ to: ["customer@example.net"],
140
+ template_id: "template_123",
141
+ subject: "Order update",
142
+ template_variables: [
143
+ { key: "CUSTOMER_NAME", type: "string", value: "Taylor" },
144
+ { key: "ORDER_NUMBER", type: "number", value: 12_345 }
145
+ ],
146
+ idempotency_key: "template-order-123"
147
+ )
148
+ ```
149
+
150
+ `from`, `to`, and `template_id` are required. Template variables are arrays of hashes containing `key`, `type`, and `value`. Number values must be Ruby numeric values rather than numeric strings.
151
+
152
+ Template sending also accepts `type`, `event_name`, `email_topic_id`, `reply_to`, `cc`, `bcc`, `headers`, `attachments`, and `idempotency_key`.
153
+
154
+ ### Attachments
155
+
156
+ An attachment uses `name` and exactly one of `path` or Base64-encoded `content`. Optional fields are `mime`, `content_id`, and `content_disposition`. `content_disposition` supports `attachment` and `inline`. Use `content_id` to reference an inline attachment from HTML.
157
+
158
+ ```ruby
159
+ require "base64"
160
+
161
+ encoded_content = Base64.strict_encode64("Example attachment\n".b)
162
+
163
+ result = client.emails.send(
164
+ from: "Acme <hello@example.com>",
165
+ to: ["customer@example.net"],
166
+ subject: "Your file",
167
+ text: "The requested file is attached.",
168
+ attachments: [{
169
+ name: "message.txt",
170
+ content: encoded_content,
171
+ mime: "text/plain",
172
+ content_disposition: "attachment"
173
+ }]
174
+ )
175
+ ```
176
+
177
+ A remote attachment can use `path` instead:
178
+
179
+ ```ruby
180
+ attachments = [{
181
+ name: "invoice.pdf",
182
+ path: "https://files.example.com/invoice.pdf",
183
+ mime: "application/pdf",
184
+ content_disposition: "attachment"
185
+ }]
186
+ ```
187
+
188
+ Do not send `path` and `content` together. `download_url`, attachment `id`, and `size` are response fields and must not be used in requests.
189
+
190
+ ### Idempotency
191
+
192
+ Pass a stable, unique idempotency key when a send might be retried:
193
+
194
+ ```ruby
195
+ result = client.emails.send(
196
+ from: "Acme <hello@example.com>",
197
+ to: ["customer@example.net"],
198
+ subject: "Receipt",
199
+ text: "Your receipt is ready.",
200
+ idempotency_key: "receipt-order-123"
201
+ )
202
+ ```
203
+
204
+ The key is sent in the `Idempotency-Key` header and is never placed in the JSON response model.
205
+
206
+ ### Verify an Email Address
207
+
208
+ ```ruby
209
+ verification = client.emails.verify("customer@example.net")
210
+ puts verification["status"]
211
+ puts verification["score"]
212
+ puts verification["domain_exists"]
213
+ puts verification["valid_syntax"]
214
+ ```
215
+
216
+ Verification results can also contain `disposable`, `role_based`, `has_mailbox`, `receive_email`, `mx_records`, and `belongs_to`. Verification is deliberately never retried.
217
+
218
+ ### List Emails
219
+
220
+ ```ruby
221
+ first = client.emails.list(per_page: 20)
222
+ items = first["data"]
223
+ pagination = first["pagination"]
224
+
225
+ if pagination["has_more"] && !items.empty?
226
+ next_page = client.emails.list(per_page: 20, after: items.last["id"])
227
+ end
228
+
229
+ unless items.empty?
230
+ previous_page = client.emails.list(per_page: 20, before: items.first["id"])
231
+ end
232
+ ```
233
+
234
+ Email list items include `id`, `status`, `subject`, `event_name`, `type`, `created_at`, `sent_at`, `recipients_count`, and `attachments_count`.
235
+
236
+ ### Get an Email
237
+
238
+ ```ruby
239
+ email = client.emails.get("email_123")
240
+
241
+ email["recipients"].each do |recipient|
242
+ puts "#{recipient['email_address']}: #{recipient['status']}"
243
+ end
244
+
245
+ email["attachments"].each do |attachment|
246
+ puts "#{attachment['name']}: #{attachment['download_url']}"
247
+ end
248
+ ```
249
+
250
+ Detailed recipients expose delivery, open, click, bounce, complaint, failure, suppression, and timestamp fields returned by the API. Detailed attachments include `id`, `name`, `mime`, `content_id`, `content_disposition`, `size`, and `download_url`.
251
+
252
+ ## Domains
253
+
254
+ ```ruby
255
+ first = client.domains.list(per_page: 20)
256
+ items = first["data"]
257
+
258
+ if first["pagination"]["has_more"] && !items.empty?
259
+ next_page = client.domains.list(per_page: 20, after: items.last["id"])
260
+ end
261
+
262
+ unless items.empty?
263
+ previous_page = client.domains.list(per_page: 20, before: items.first["id"])
264
+ end
265
+
266
+ domain = client.domains.get("domain_123")
267
+ verification = client.domains.verify("example.com")
268
+ ```
269
+
270
+ `list` returns domain models and pagination. `get(id)` returns `id`, `domain_name`, `status`, and `created_at`. `verify(domain)` returns the verification status and is never retried.
271
+
272
+ ## Contacts
273
+
274
+ ```ruby
275
+ contact = client.contacts.create(
276
+ email: "customer@example.net",
277
+ first_name: "Taylor",
278
+ last_name: "Morgan",
279
+ phone: "+14165550123",
280
+ is_globally_unsubscribed: false,
281
+ categories: ["category_123"],
282
+ email_topics: ["topic_123"]
283
+ )
284
+
285
+ client.contacts.update(
286
+ contact["id"],
287
+ first_name: "Alex",
288
+ categories: ["category_456"],
289
+ sync_categories: true
290
+ )
291
+
292
+ stored = client.contacts.get(contact["id"])
293
+ client.contacts.delete(contact["id"])
294
+ ```
295
+
296
+ `update` also supports `last_name`, `phone`, `is_globally_unsubscribed`, `email_topics`, and `sync_email_topics`.
297
+
298
+ Contact pagination:
299
+
300
+ ```ruby
301
+ first = client.contacts.list(per_page: 20)
302
+ items = first["data"]
303
+ next_page = client.contacts.list(per_page: 20, after: items.last["id"]) if first["pagination"]["has_more"] && !items.empty?
304
+ previous_page = client.contacts.list(per_page: 20, before: items.first["id"]) unless items.empty?
305
+ ```
306
+
307
+ ## Contact Categories
308
+
309
+ ```ruby
310
+ category = client.contact_categories.create(name: "Customers", slug: "customers")
311
+ category = client.contact_categories.get(category["id"])
312
+ client.contact_categories.update(category["id"], name: "Active customers", slug: "active-customers")
313
+ client.contact_categories.delete(category["id"])
314
+ ```
315
+
316
+ Contact-category pagination:
317
+
318
+ ```ruby
319
+ first = client.contact_categories.list(per_page: 20)
320
+ items = first["data"]
321
+ next_page = client.contact_categories.list(per_page: 20, after: items.last["id"]) if first["pagination"]["has_more"] && !items.empty?
322
+ previous_page = client.contact_categories.list(per_page: 20, before: items.first["id"]) unless items.empty?
323
+ ```
324
+
325
+ ## Email Topics
326
+
327
+ ```ruby
328
+ topic = client.email_topics.create(
329
+ name: "Product updates",
330
+ slug: "product-updates",
331
+ auto_subscribe: false,
332
+ public: true,
333
+ description: "News about product improvements",
334
+ domain_id: "domain_123"
335
+ )
336
+
337
+ topic = client.email_topics.get(topic["id"])
338
+ client.email_topics.update(
339
+ topic["id"],
340
+ name: "Product news",
341
+ description: "Important product news",
342
+ public: true,
343
+ auto_subscribe: false
344
+ )
345
+ client.email_topics.delete(topic["id"])
346
+ ```
347
+
348
+ When `domain_id` is supplied, the request sends it as `domain: { id: domain_id }`. Responses may include a nested domain with `id` and `name`.
349
+
350
+ Email-topic pagination:
351
+
352
+ ```ruby
353
+ first = client.email_topics.list(per_page: 20)
354
+ items = first["data"]
355
+ next_page = client.email_topics.list(per_page: 20, after: items.last["id"]) if first["pagination"]["has_more"] && !items.empty?
356
+ previous_page = client.email_topics.list(per_page: 20, before: items.first["id"]) unless items.empty?
357
+ ```
358
+
359
+ ## Model Serialization and Database Storage
360
+
361
+ Every API response, list envelope, pagination object, list item, and nested response hash is a `LetMeSendEmail::Models::Model`. Arrays contain recursively wrapped models. Access fields with string or symbol keys:
362
+
363
+ ```ruby
364
+ contact = client.contacts.get("contact_123")
365
+ puts contact["email"]
366
+ puts contact[:email]
367
+ ```
368
+
369
+ Use `to_h` for a recursive defensive copy suitable for persistence:
370
+
371
+ ```ruby
372
+ contact = client.contacts.get("contact_123")
373
+ database_record = contact.to_h
374
+
375
+ # Pass database_record to your application's persistence layer.
376
+ database_record["synced_at"] = Time.now.utc.iso8601
377
+ ```
378
+
379
+ Use the standard JSON library directly:
380
+
381
+ ```ruby
382
+ require "json"
383
+
384
+ json_document = JSON.generate(contact)
385
+ restored = JSON.parse(json_document)
386
+ ```
387
+
388
+ `as_json` returns the same plain representation for Rails integrations. Field names remain API snake-case strings. Nested models and arrays convert recursively; `nil`, booleans, numbers, identifiers, enum strings, and ISO 8601 timestamp strings are preserved. `to_h` returns new hashes, arrays, and strings so mutating the result does not mutate the SDK model. Client configuration, API keys, authorization headers, webhook secrets, retries, and transport state are never part of response models.
389
+
390
+ Request inputs are ordinary Ruby hashes and arrays, so they are already compatible with `JSON.generate`. The SDK omits optional keyword fields that are not supplied.
391
+
392
+ ## Pagination
393
+
394
+ `emails`, `domains`, `contacts`, `contact_categories`, and `email_topics` list methods accept `per_page:`, `after:`, and `before:`. A list response contains:
395
+
396
+ - `data`: the current array of models
397
+ - `pagination.has_more`: whether a next page is available
398
+ - `pagination.per_page`: requested page size
399
+ - `pagination.fetched`: number fetched
400
+ - `pagination.total`: total matching records
401
+
402
+ Use the final item ID as `after` and the first item ID as `before`. Always guard an empty collection, check `has_more` before moving forward, and never pass `after` with `before`. The client rejects that combination before network I/O.
403
+
404
+ The API does not expose arbitrary page-number jumps or `has_previous`. Retain cursor history in the application when implementing Back/Previous navigation. The SDK returns one page per call and does not auto-paginate.
405
+
406
+ ## Errors and Exceptions
407
+
408
+ All SDK errors inherit from `LetMeSendEmail::Error`:
409
+
410
+ | Error | Meaning |
411
+ |---|---|
412
+ | `ValidationError` | HTTP 400, 413, or 422 |
413
+ | `AuthenticationError` | HTTP 401 |
414
+ | `AuthorizationError` | HTTP 403 |
415
+ | `NotFoundError` | HTTP 404 |
416
+ | `ConflictError` | HTTP 409 |
417
+ | `RateLimitError` | HTTP 429 |
418
+ | `ApiError` | Other API errors or malformed success responses |
419
+ | `NetworkError` | Connection, TLS, socket, or response transport failure |
420
+ | `TimeoutError` | Connection, read, or write timeout |
421
+ | `WebhookVerificationError` | Invalid webhook headers, timestamp, signature, or JSON |
422
+ | `WebhookSigningError` | Missing or invalid webhook secret |
423
+
424
+ ```ruby
425
+ begin
426
+ client.emails.get("email_123")
427
+ rescue LetMeSendEmail::ValidationError => e
428
+ warn e.validation_errors.inspect
429
+ rescue LetMeSendEmail::AuthenticationError, LetMeSendEmail::AuthorizationError => e
430
+ warn e.message
431
+ rescue LetMeSendEmail::NotFoundError, LetMeSendEmail::ConflictError => e
432
+ warn "#{e.message} (HTTP #{e.status_code})"
433
+ rescue LetMeSendEmail::RateLimitError => e
434
+ warn "Retry after: #{e.retry_after.inspect}"
435
+ rescue LetMeSendEmail::NetworkError, LetMeSendEmail::TimeoutError => e
436
+ warn "Transport failure: #{e.message}"
437
+ rescue LetMeSendEmail::ApiError => e
438
+ warn "API failure: #{e.message}"
439
+ end
440
+ ```
441
+
442
+ `Error` exposes `status_code`, `api_code`, `validation_errors`, `request_id`, `response_headers`, and `raw_body`. `RateLimitError` additionally exposes `retry_after`, `limit`, `remaining`, and `reset_at`. Treat `raw_body` and headers as diagnostic data; avoid logging message content or other sensitive application data.
443
+
444
+ ## Timeouts, Cancellation, and Retries
445
+
446
+ The configured positive `timeout_ms` value applies separately to the `net/http` connect, response-read, and request-write phases. Ruby `net/http` does not expose a separate SDK cancellation token. Applications can stop waiting by cancelling or terminating their own task/thread according to their framework's conventions.
447
+
448
+ Retries are disabled by default. Configure them explicitly and use idempotency keys for sends. A timeout or network failure on a non-idempotent write without a key is returned immediately because the server may have accepted the request.
449
+
450
+ For HTTP 429, inspect `RateLimitError#retry_after`. Missing, malformed, zero, negative, past, or greater-than-300-second values are exposed as `nil` and are not automatically retried.
451
+
452
+ ## Webhooks
453
+
454
+ Verification requires the exact raw request body and these headers:
455
+
456
+ - `webhook-id`
457
+ - `webhook-log-id`
458
+ - `webhook-timestamp`
459
+ - `webhook-signature`
460
+
461
+ The secret is Base64 encoded and may use the `whsec_` prefix. Only `v1` signatures are accepted; multiple signatures are supported and unknown versions are ignored. The default timestamp tolerance is 300 seconds in either direction.
462
+
463
+ ```ruby
464
+ require "json"
465
+ require "letmesendemail"
466
+
467
+ secret = ENV.fetch("LETMESENDEMAIL_WEBHOOK_SECRET")
468
+ raise "LETMESENDEMAIL_WEBHOOK_SECRET must not be empty" if secret.empty?
469
+
470
+ raw_body = $stdin.read
471
+ headers = JSON.parse(ENV.fetch("LETMESENDEMAIL_WEBHOOK_HEADERS_JSON"))
472
+
473
+ begin
474
+ verified_payload = LetMeSendEmail::Webhooks.verify(raw_body, headers, secret)
475
+ puts verified_payload.inspect
476
+ rescue LetMeSendEmail::WebhookVerificationError,
477
+ LetMeSendEmail::WebhookSigningError => e
478
+ warn "Webhook rejected: #{e.message}"
479
+ end
480
+ ```
481
+
482
+ Rack/CGI header keys such as `HTTP_WEBHOOK_ID` are normalized automatically. The verifier checks the HMAC in constant time, then parses the payload and requires a JSON object. It does not assume any event names or payload schema.
483
+
484
+ ## Framework Integration
485
+
486
+ The client is a plain Ruby object and can be registered in the dependency container used by Rails, Hanami, Sinatra, or another framework. Create it once per application configuration, or per tenant when API keys differ. Do not expose the client or its API key through model serialization or logs.
487
+
488
+ ## Testing
489
+
490
+ This gem does not provide a fake client. Place the client behind an application service boundary and inject a test double for that service in application tests. To contribute to the gem:
491
+
492
+ ```bash
493
+ bundle install
494
+ bundle exec rubocop --parallel
495
+ bundle exec rspec
496
+ find examples -name "*.rb" -print0 | xargs -0 -n1 ruby -c
497
+ gem build letmesendemail.gemspec
498
+ ```
499
+
500
+ ## Runtime Support
501
+
502
+ Ruby 3.1, 3.2, 3.3, 3.4, and 4.0 are tested in CI. The gemspec requires Ruby 3.1 or newer.
503
+
504
+ ## Upgrading
505
+
506
+ Review the [changelog](../CHANGELOG.md) before upgrading. Install an explicit
507
+ version when your application requires reproducible dependency resolution.
508
+
509
+ ## Getting Help
510
+
511
+ - [letmesend.email documentation](https://letmesend.email/docs)
512
+ - [Issue tracker](https://github.com/letmesendemail/letmesendemail-ruby/issues)
513
+ - [Changelog](../CHANGELOG.md)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'letmesendemail'
5
+
6
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
7
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
8
+
9
+ content = Base64.strict_encode64("Example attachment\n".b)
10
+ client = LetMeSendEmail::Client.new(api_key: api_key)
11
+ result = client.emails.send(
12
+ from: 'Acme <hello@example.com>',
13
+ to: ['customer@example.net'],
14
+ subject: 'Your file',
15
+ text: 'The requested file is attached.',
16
+ attachments: [{
17
+ name: 'message.txt', content: content, mime: 'text/plain',
18
+ content_disposition: 'attachment'
19
+ }]
20
+ )
21
+ puts result['id']
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'letmesendemail'
4
+
5
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
6
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
7
+
8
+ client = LetMeSendEmail::Client.new(api_key: api_key)
9
+
10
+ begin
11
+ client.emails.get('email_123')
12
+ rescue LetMeSendEmail::ValidationError => e
13
+ warn "Validation failed: #{e.validation_errors.inspect}"
14
+ rescue LetMeSendEmail::RateLimitError => e
15
+ warn "Rate limited; retry after: #{e.retry_after.inspect}"
16
+ rescue LetMeSendEmail::Error => e
17
+ warn "Request failed: #{e.message} (request #{e.request_id || 'unknown'})"
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'letmesendemail'
5
+
6
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
7
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
8
+
9
+ client = LetMeSendEmail::Client.new(api_key: api_key)
10
+ result = client.emails.send(
11
+ from: 'Acme <hello@example.com>',
12
+ to: ['customer@example.net'],
13
+ subject: 'Receipt',
14
+ text: 'Your receipt is ready.',
15
+ idempotency_key: SecureRandom.uuid
16
+ )
17
+ puts result['id']
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'letmesendemail'
4
+
5
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
6
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
7
+
8
+ client = LetMeSendEmail::Client.new(api_key: api_key)
9
+ first_page = client.emails.list(per_page: 20)
10
+ items = first_page['data']
11
+
12
+ if first_page['pagination']['has_more'] && !items.empty?
13
+ next_page = client.emails.list(per_page: 20, after: items.last['id'])
14
+ puts "Next page contains #{next_page['data'].length} emails"
15
+ end
16
+
17
+ unless items.empty?
18
+ previous_page = client.emails.list(per_page: 20, before: items.first['id'])
19
+ puts "Previous page contains #{previous_page['data'].length} emails"
20
+ end
data/examples/send.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'letmesendemail'
4
+
5
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
6
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
7
+
8
+ client = LetMeSendEmail::Client.new(api_key: api_key)
9
+ result = client.emails.send(
10
+ from: 'Acme <hello@example.com>',
11
+ to: ['customer@example.net'],
12
+ subject: 'Welcome',
13
+ html: '<p>Thanks for joining.</p>',
14
+ text: 'Thanks for joining.'
15
+ )
16
+ puts result['id']
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'letmesendemail'
4
+
5
+ api_key = ENV.fetch('LETMESENDEMAIL_API_KEY')
6
+ raise 'LETMESENDEMAIL_API_KEY must not be empty' if api_key.empty?
7
+
8
+ client = LetMeSendEmail::Client.new(api_key: api_key)
9
+ result = client.emails.send_with_template(
10
+ from: 'Acme <hello@example.com>',
11
+ to: ['customer@example.net'],
12
+ template_id: 'template_123',
13
+ template_variables: [
14
+ { key: 'CUSTOMER_NAME', type: 'string', value: 'Taylor' },
15
+ { key: 'ORDER_NUMBER', type: 'number', value: 12_345 }
16
+ ]
17
+ )
18
+ puts result['status']
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'letmesendemail'
5
+
6
+ secret = ENV.fetch('LETMESENDEMAIL_WEBHOOK_SECRET')
7
+ raise 'LETMESENDEMAIL_WEBHOOK_SECRET must not be empty' if secret.empty?
8
+
9
+ headers = JSON.parse(ENV.fetch('LETMESENDEMAIL_WEBHOOK_HEADERS_JSON'))
10
+ raw_payload = $stdin.read
11
+ verified_payload = LetMeSendEmail::Webhooks.verify(raw_payload, headers, secret)
12
+ puts JSON.generate(verified_payload)