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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.md +21 -0
- data/README.md +220 -0
- data/docs/docs.md +513 -0
- data/examples/attachments.rb +21 -0
- data/examples/errors.rb +18 -0
- data/examples/idempotency.rb +17 -0
- data/examples/pagination.rb +20 -0
- data/examples/send.rb +16 -0
- data/examples/template.rb +18 -0
- data/examples/webhooks.rb +12 -0
- data/lib/letmesendemail/client.rb +109 -0
- data/lib/letmesendemail/config.rb +80 -0
- data/lib/letmesendemail/errors.rb +60 -0
- data/lib/letmesendemail/http_transport.rb +129 -0
- data/lib/letmesendemail/models.rb +99 -0
- data/lib/letmesendemail/resources/base.rb +31 -0
- data/lib/letmesendemail/resources/contact_categories.rb +45 -0
- data/lib/letmesendemail/resources/contacts.rb +60 -0
- data/lib/letmesendemail/resources/domains.rb +28 -0
- data/lib/letmesendemail/resources/email_topics.rb +53 -0
- data/lib/letmesendemail/resources/emails.rb +76 -0
- data/lib/letmesendemail/version.rb +6 -0
- data/lib/letmesendemail/webhooks.rb +109 -0
- data/lib/letmesendemail.rb +15 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f051881d8ab62e6dfe9b77358ad11248ea80b39c9daabd76f99ec241d82815ef
|
|
4
|
+
data.tar.gz: d27abe88c1782cbfa48ac368e31aa8022495a1181666697c626d1787ede676d7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e22b56818027da8277ffacc91c91b5358e9c9b5a797a554fb6b66c571cfe805135cde03719ddf22e044b895af2c51481569deb5f451a4860a33c653848f17357
|
|
7
|
+
data.tar.gz: cd94300002f8d73c4250da3f14fa3295c4653e01e5f2df2cd3959f3c0386ac4734b694ab90522a8c5642c090dd43d23550551414ba8282019ecc9968e6ffe372
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-07-13
|
|
4
|
+
|
|
5
|
+
- Initial Ruby SDK implementation for emails, domains, contacts, contact categories, and email topics.
|
|
6
|
+
- Structured API, network, timeout, rate-limit, and webhook errors.
|
|
7
|
+
- Validated client configuration, safe resource path encoding, conservative retries, and `Retry-After` support.
|
|
8
|
+
- Constant-time webhook signature verification with ordinary and Rack/CGI header support.
|
|
9
|
+
- Recursive response models with `to_h`, `as_json`, and standard JSON serialization.
|
|
10
|
+
- Fixture-backed tests, runnable examples, CI, comprehensive documentation, and RubyGems release guidance.
|
|
11
|
+
- Webhook verification tests use generic payload data without assuming undocumented
|
|
12
|
+
event names or payload fields.
|
|
13
|
+
- Development dependencies constrain `parallel` to the Ruby 3.1-compatible 1.x
|
|
14
|
+
series so the complete Ruby 3.1–4.0 CI matrix resolves consistently.
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Apsonex Inc. <info@apsonex.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# letmesend.email SDK for Ruby
|
|
2
|
+
|
|
3
|
+
The official Ruby client for the [letmesend.email](https://letmesend.email/) API. It supports Ruby 3.1+, uses `net/http`, and has no runtime HTTP dependency.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your bundle:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add letmesendemail
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install it directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install letmesendemail
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "letmesendemail"
|
|
23
|
+
|
|
24
|
+
api_key = ENV.fetch("LETMESENDEMAIL_API_KEY")
|
|
25
|
+
raise "LETMESENDEMAIL_API_KEY must not be empty" if api_key.empty?
|
|
26
|
+
|
|
27
|
+
client = LetMeSendEmail::Client.new(api_key: api_key)
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
email = client.emails.send(
|
|
31
|
+
from: "Acme <hello@example.com>",
|
|
32
|
+
to: ["customer@example.net"],
|
|
33
|
+
subject: "Welcome",
|
|
34
|
+
html: "<p>Thanks for joining.</p>",
|
|
35
|
+
text: "Thanks for joining."
|
|
36
|
+
)
|
|
37
|
+
puts email["id"]
|
|
38
|
+
rescue LetMeSendEmail::Error => e
|
|
39
|
+
warn "Email request failed: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
config = LetMeSendEmail::Config.new(api_key)
|
|
47
|
+
config.base_url = "https://letmesend.email/api/v1"
|
|
48
|
+
config.timeout_ms = 30_000
|
|
49
|
+
config.retries = 2
|
|
50
|
+
|
|
51
|
+
client = LetMeSendEmail::Client.new(config: config)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The default timeout is 30,000 milliseconds and retries default to zero. Retry counts must be between 0 and 20. Safe requests retry network errors, timeouts, HTTP 408/500/502/503/504, and HTTP 429 only when `Retry-After` is valid and no more than 300 seconds. Email sends require an idempotency key to retry; verification calls are never retried.
|
|
55
|
+
|
|
56
|
+
## Emails
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
sent = client.emails.send(
|
|
60
|
+
from: "Acme <hello@example.com>",
|
|
61
|
+
to: ["customer@example.net"],
|
|
62
|
+
subject: "Receipt",
|
|
63
|
+
text: "Your receipt is ready.",
|
|
64
|
+
idempotency_key: "receipt-order-123"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
templated = client.emails.send_with_template(
|
|
68
|
+
from: "Acme <hello@example.com>",
|
|
69
|
+
to: ["customer@example.net"],
|
|
70
|
+
template_id: "template_123",
|
|
71
|
+
template_variables: [
|
|
72
|
+
{ key: "CUSTOMER_NAME", type: "string", value: "Taylor" },
|
|
73
|
+
{ key: "ORDER_NUMBER", type: "number", value: 12_345 }
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
verification = client.emails.verify("customer@example.net")
|
|
78
|
+
email = client.emails.get("email_123")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Domains
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
domains = client.domains.list(per_page: 20)
|
|
85
|
+
domain = client.domains.get("domain_123")
|
|
86
|
+
verification = client.domains.verify("example.com")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Contacts
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
contact = client.contacts.create(
|
|
93
|
+
email: "customer@example.net",
|
|
94
|
+
first_name: "Taylor",
|
|
95
|
+
categories: ["category_123"]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
client.contacts.update(contact["id"], first_name: "Morgan")
|
|
99
|
+
client.contacts.get(contact["id"])
|
|
100
|
+
client.contacts.list(per_page: 20)
|
|
101
|
+
client.contacts.delete(contact["id"])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Contact Categories
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
category = client.contact_categories.create(name: "Customers")
|
|
108
|
+
client.contact_categories.get(category["id"])
|
|
109
|
+
client.contact_categories.list(per_page: 20)
|
|
110
|
+
client.contact_categories.update(category["id"], name: "Active customers")
|
|
111
|
+
client.contact_categories.delete(category["id"])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Email Topics
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
topic = client.email_topics.create(
|
|
118
|
+
name: "Product updates",
|
|
119
|
+
slug: "product-updates",
|
|
120
|
+
public: true,
|
|
121
|
+
auto_subscribe: false
|
|
122
|
+
)
|
|
123
|
+
client.email_topics.get(topic["id"])
|
|
124
|
+
client.email_topics.list(per_page: 20)
|
|
125
|
+
client.email_topics.update(topic["id"], description: "Product news")
|
|
126
|
+
client.email_topics.delete(topic["id"])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Pagination
|
|
130
|
+
|
|
131
|
+
All list resources accept `per_page`, `after`, and `before`. Do not send `after` and `before` together.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
first = client.emails.list(per_page: 20)
|
|
135
|
+
items = first["data"]
|
|
136
|
+
|
|
137
|
+
if first["pagination"]["has_more"] && !items.empty?
|
|
138
|
+
next_page = client.emails.list(per_page: 20, after: items.last["id"])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
unless items.empty?
|
|
142
|
+
previous_page = client.emails.list(per_page: 20, before: items.first["id"])
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Retain cursor history in your application for Back/Previous navigation. The API does not provide arbitrary page-number jumps.
|
|
147
|
+
|
|
148
|
+
## Model Serialization
|
|
149
|
+
|
|
150
|
+
API operations return `LetMeSendEmail::Models::Model`. It supports hash-style access, recursive `to_h`, Rails-compatible `as_json`, and standard JSON encoding.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
contact = client.contacts.get("contact_123")
|
|
154
|
+
database_attributes = contact.to_h
|
|
155
|
+
json = JSON.generate(contact)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`to_h` returns a defensive recursive copy. It preserves API field names and `nil` values while excluding client configuration, credentials, and transport state.
|
|
159
|
+
|
|
160
|
+
## Error Handling
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
begin
|
|
164
|
+
client.emails.get("email_123")
|
|
165
|
+
rescue LetMeSendEmail::ValidationError => e
|
|
166
|
+
warn e.validation_errors.inspect
|
|
167
|
+
rescue LetMeSendEmail::AuthenticationError, LetMeSendEmail::AuthorizationError => e
|
|
168
|
+
warn e.message
|
|
169
|
+
rescue LetMeSendEmail::RateLimitError => e
|
|
170
|
+
warn "Retry after #{e.retry_after.inspect} seconds"
|
|
171
|
+
rescue LetMeSendEmail::NetworkError, LetMeSendEmail::TimeoutError => e
|
|
172
|
+
warn e.message
|
|
173
|
+
rescue LetMeSendEmail::Error => e
|
|
174
|
+
warn "#{e.message}; request_id=#{e.request_id.inspect}"
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Webhooks
|
|
179
|
+
|
|
180
|
+
Always verify the exact, unmodified request body before using the parsed payload.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
require "json"
|
|
184
|
+
|
|
185
|
+
secret = ENV.fetch("LETMESENDEMAIL_WEBHOOK_SECRET")
|
|
186
|
+
raise "LETMESENDEMAIL_WEBHOOK_SECRET must not be empty" if secret.empty?
|
|
187
|
+
raw_request_body = $stdin.read
|
|
188
|
+
request_headers = JSON.parse(ENV.fetch("LETMESENDEMAIL_WEBHOOK_HEADERS_JSON"))
|
|
189
|
+
|
|
190
|
+
begin
|
|
191
|
+
payload = LetMeSendEmail::Webhooks.verify(raw_request_body, request_headers, secret)
|
|
192
|
+
puts payload.inspect
|
|
193
|
+
rescue LetMeSendEmail::WebhookVerificationError,
|
|
194
|
+
LetMeSendEmail::WebhookSigningError => e
|
|
195
|
+
warn "Webhook rejected: #{e.message}"
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The verifier accepts ordinary headers and Rack/CGI `HTTP_WEBHOOK_*` keys. The default timestamp tolerance is 300 seconds.
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
The SDK does not make network requests until a resource method is called. Use application-level dependency boundaries around the client in tests. To work on the gem:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
bundle exec rubocop
|
|
207
|
+
bundle exec rspec
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Version Support
|
|
211
|
+
|
|
212
|
+
Ruby 3.1, 3.2, 3.3, 3.4, and 4.0 are tested in CI.
|
|
213
|
+
|
|
214
|
+
## Changelog
|
|
215
|
+
|
|
216
|
+
See the [changelog](CHANGELOG.md).
|
|
217
|
+
|
|
218
|
+
## Full Documentation
|
|
219
|
+
|
|
220
|
+
See the [complete Ruby SDK manual](docs/docs.md).
|