misarmail 1.0.0 → 5.0.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +392 -38
  3. data/lib/misar_mail/client.rb +6 -1
  4. metadata +13 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fb5ef6ef30c000df0e5737eac0e056e77db86562b67199aa0c53f4097846ba6
4
- data.tar.gz: 37092f3124bab5b9fedad156bac2199e99c4c4115121f7277ee1103363a013b4
3
+ metadata.gz: c36f75bbe102b5c13546df99d0caa263e59ce8b88fe46d723f7d02810d65b477
4
+ data.tar.gz: 278a9e3c983715766e847e8a56bbebbeec4c4648b41e0956a615b3bc24249760
5
5
  SHA512:
6
- metadata.gz: 88f14c434908fe0f189680689f173de703ae22903c46cbe1377ebdcffa294b60e2e99ed10fbd909d8ac07967149db4f77e1b8e29c7de8651ff0e860d75908d2d
7
- data.tar.gz: dad272747a6a1d4a9d492ef25f6d2e28a218a14bc3a937d7aff2b87367c47b6f3b486c2a592bdcaff828b32a7f1a1c1fa7ee441c8d3a4c1d7ff59bc29b25f346
6
+ metadata.gz: 0b98b6228ed597e0c49c86827c16e71cfb4925f7efea1ca341f653efe7ca128992e0e775fc0f01ed687af0bd06ffc6d4c4e92c40767dd5694f1e7ae32038292d
7
+ data.tar.gz: a82c543e3faf7e792bff37b5233539d8c34643cf6ae29c9c97624e3d5775f73e827d0a1a1244c1eb2510cb5f9507852ad31ff218d5a99922c4801c1aea45aabe
data/README.md CHANGED
@@ -1,65 +1,386 @@
1
1
  # MisarMail Ruby SDK
2
2
 
3
- Official Ruby SDK for the [MisarMail](https://misarmail.com) APItransactional
4
- send, campaigns, contacts, templates, automations, deliverability, warmup,
5
- monetization and the two AI streams.
3
+ > Send transactional email and run marketing campaigns from Ruby one client, no runtime dependencies.
6
4
 
7
- Full reference: [`misarmail.com/docs`](https://misarmail.com/docs).
5
+ [![gem](https://img.shields.io/gem/v/misarmail)](https://rubygems.org/gems/misarmail)
6
+ [![ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-CC342D)](https://rubygems.org/gems/misarmail)
7
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
8
+
9
+ **33 resource groups · 90 methods · SSE streaming · webhook signature verification**
10
+
11
+ MisarMail is one API for both halves of your email: the receipts and password resets your product sends, and the campaigns, segments and automations your marketing team runs on the same contact list and the same verified domains.
12
+
13
+ Built on `net/http` with nothing outside the standard library, for Ruby 2.7+. Every method returns a plain `Hash` with string keys, so nothing breaks when the API adds a field.
14
+
15
+ ---
8
16
 
9
17
  ## Install
10
18
 
19
+ ### RubyGems
20
+
11
21
  ```bash
12
22
  gem install misarmail
13
23
  ```
14
24
 
15
- ## Auth
25
+ ### Bundler
26
+
27
+ ```ruby
28
+ gem "misarmail", "~> 1.0"
29
+ ```
30
+
31
+ ---
16
32
 
17
- Use a MisarMail developer key (`msk_…`), created at
18
- [misarmail.com/developers](https://misarmail.com/developers). It is sent as
19
- `Authorization: Bearer msk_…`.
33
+ ## Authentication
34
+
35
+ Create a developer key at https://mail.misar.io/developers. It starts with `msk_` and is
36
+ sent as `Authorization: Bearer msk_…`.
20
37
 
21
38
  Every call is metered against the subscription attached to that key. There is no
22
- client-side limit checking — the server decides, and the SDK surfaces its answer.
39
+ client-side limit checking — the server decides, and the SDK surfaces its answer. A plan
40
+ refusal answers **403** with `code: "plan_limit_exceeded"` and is never retried.
41
+
42
+ ```ruby
43
+ require "misar_mail"
44
+
45
+ mail = MisarMail::Client.new(api_key: ENV.fetch("MISARMAIL_API_KEY"))
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Resources
51
+
52
+ Every group the client exposes, and every public method on it.
53
+
54
+ ### Send
55
+
56
+ | Resource | Methods | What it covers |
57
+ | --- | --- | --- |
58
+ | `mail.email` | `send` | Transactional send — cc/bcc/reply-to, tags, metadata, `idempotency_key`. |
59
+ | `mail.sandbox` | `send`, `list`, `delete` | Test sends captured instead of delivered. |
60
+
61
+ ### Campaigns and tests
62
+
63
+ | Resource | Methods | What it covers |
64
+ | --- | --- | --- |
65
+ | `mail.campaigns` | `list`, `create`, `get`, `update`, `send`, `delete` | Marketing campaigns: draft, edit, queue for send. |
66
+ | `mail.ab_tests` | `list`, `create`, `get`, `set_winner` | Subject, content, send-time, from-name and preheader splits, and winner selection. |
67
+
68
+ ### Audience
69
+
70
+ | Resource | Methods | What it covers |
71
+ | --- | --- | --- |
72
+ | `mail.contacts` | `list`, `create`, `get`, `update`, `delete`, `import_contacts` | Subscribers, plus bulk import. |
73
+ | `mail.segments` | `members` | Dynamic audience segments and their membership. |
74
+ | `mail.landing_pages` | `create` | Hosted landing pages with an email capture form. |
75
+
76
+ ### Content
77
+
78
+ | Resource | Methods | What it covers |
79
+ | --- | --- | --- |
80
+ | `mail.templates` | `list`, `create`, `get`, `update`, `delete`, `render` | Reusable templates and server-side variable rendering. |
81
+ | `mail.ai` | `subject_lines` | AI-generated subject lines. |
82
+
83
+ ### Automations
84
+
85
+ | Resource | Methods | What it covers |
86
+ | --- | --- | --- |
87
+ | `mail.automations` | `list`, `create`, `get`, `update`, `delete`, `activate` | Trigger-based workflows — welcome series, drips, re-engagement. |
88
+
89
+ ### Deliverability and sending infrastructure
90
+
91
+ | Resource | Methods | What it covers |
92
+ | --- | --- | --- |
93
+ | `mail.domains` | `list`, `create`, `get`, `verify`, `delete` | Sending domains and their DNS verification. |
94
+ | `mail.dmarc` | `check`, `list_domains`, `add_domain`, `remove_domain` | Live SPF/DKIM/DMARC record checks and monitored domains. |
95
+ | `mail.deliverability` | `audit`, `score` | Deliverability score, audit and remediation guidance. |
96
+ | `mail.dedicated_ips` | `list`, `create`, `update`, `delete` | Dedicated sending IPs. |
97
+ | `mail.warmup` | `get` | IP/domain warm-up progress and today's remaining capacity. |
98
+ | `mail.inbound` | `list`, `create`, `get`, `delete` | Inbound routing domains, so replies land in the unified inbox. |
99
+
100
+ ### Mailbox and inbox
101
+
102
+ | Resource | Methods | What it covers |
103
+ | --- | --- | --- |
104
+ | `mail.emails` | `list`, `get`, `update` | Stored messages in the mailbox. |
105
+ | `mail.email_accounts` | `list` | Connected mailbox accounts. |
106
+
107
+ ### Analytics and attribution
108
+
109
+ | Resource | Methods | What it covers |
110
+ | --- | --- | --- |
111
+ | `mail.analytics` | `overview` | Delivery and engagement stats — aggregate, or one campaign. |
112
+ | `mail.track` | `event`, `purchase` | Custom events and ecommerce purchases. |
113
+ | `mail.revenue` | `attribution` | Revenue attributed back to email. |
114
+ | `mail.usage` | `get` | Metered usage for a period. |
115
+
116
+ ### Validation
117
+
118
+ | Resource | Methods | What it covers |
119
+ | --- | --- | --- |
120
+ | `mail.validate` | `email` | Address validation, and the credit balance behind it. |
121
+
122
+ ### Plan, billing and credits
123
+
124
+ | Resource | Methods | What it covers |
125
+ | --- | --- | --- |
126
+ | `mail.plan` | `get`, `monetization` | Current plan, quotas and monetization stats. |
127
+ | `mail.billing` | `subscription`, `checkout` | Subscription state and checkout. |
128
+ | `mail.subscription` | `get`, `upsert`, `cancel` | Subscription read/write and per-product plan limits. |
129
+ | `mail.wallet` | `get`, `credit`, `debit` | Credit balance, credit and debit. |
130
+ | `mail.credit_rates` | `list` | What each metered action costs in credits. |
131
+ | `mail.team_members` | `get` | Team members on the account. |
132
+ | `mail.monetization` | `tip` | Newsletter tips. |
133
+
134
+ ### Developer
135
+
136
+ | Resource | Methods | What it covers |
137
+ | --- | --- | --- |
138
+ | `mail.keys` | `list`, `create`, `get`, `revoke` | API keys — create, list, revoke. |
139
+ | `mail.webhooks` | `list`, `create`, `get`, `update`, `delete`, `test` | Webhook endpoints, plus a test delivery. |
140
+ | `mail.streaming` | `generate_email`, `campaign_send` | The two Server-Sent Events endpoints. |
141
+
142
+ ---
143
+
144
+ ## Client
145
+
146
+ | Thing | Detail |
147
+ | --- | --- |
148
+ | Entry point | `MisarMail::Client.new(api_key:, timeout: 30, max_retries: 3, base_url:)`. `MisarMail.new(**kwargs)` is a shorthand. |
149
+ | `base_url` | `https://api.misar.io/mail/v1` |
150
+ | Results | A `Hash` with **string** keys. `204`/empty comes back as `{}`; a top-level array is wrapped as `{"data" => [...]}`. |
151
+ | Transport | `net/http`, 10-second open timeout, your `timeout` for reads. |
152
+ | Retried | `429`, `500`, `502`, `503`, `504`, plus `Net::OpenTimeout`, `Net::ReadTimeout`, `Errno::ECONNREFUSED`, `Errno::ECONNRESET` and `SocketError` — 300 ms then 600 ms. |
153
+ | Never retried | Plan refusals, and streams. |
154
+ | Errors | `MisarMail::ApiError`, with `NetworkError` and `PlanLimitError` extending it. |
155
+ | Webhook verifier | `MisarMail::Webhooks.verify` / `.sign`. |
156
+ | Escape hatch | `mail.request(:get, path)` is public, for routes with no method yet. |
157
+
158
+ ---
23
159
 
24
160
  ## Quick start
25
161
 
26
162
  ```ruby
27
163
  require "misar_mail"
28
164
 
29
- mail = MisarMail::Client.new(api_key: "msk_your_key")
165
+ mail = MisarMail::Client.new(api_key: ENV.fetch("MISARMAIL_API_KEY"))
30
166
 
31
- mail.email.send(
32
- from: { email: "you@yourdomain.com" },
167
+ sent = mail.email.send(
168
+ from: { email: "you@yourdomain.com", name: "Your App" },
33
169
  to: [{ email: "someone@example.com" }],
34
170
  subject: "Hello",
35
171
  html: "<p>Hi there</p>"
36
172
  )
37
173
 
38
- contacts = mail.contacts.list(page: 1, limit: 50)
174
+ puts sent["message_id"]
39
175
  ```
40
176
 
41
- ## Plan limits
177
+ ## Primary functions
42
178
 
43
- Both a spent allowance and a feature that is not on the plan answer **`403`**,
44
- carrying `code: "plan_limit_exceeded"`. The SDK keys on that code rather than
45
- the status, which is why a refusal is typed correctly even though 403 is
46
- otherwise an authorization failure. The SDK raises
47
- `PlanLimitError` for either, and **does not retry** it — retrying cannot
48
- help until the allowance resets or the plan changes. Read ``upgrade_url`` to
49
- send the user somewhere useful.
179
+ ### Send a transactional email
50
180
 
51
- `GET /plan` returns `plan`, `sending` (the per-day and per-month email caps),
52
- `usage` an array with one entry per metered feature, each carrying `used`,
53
- `limit` and `remaining` — and `upgrade`, which is null until a quota is tight.
54
- A null `limit` means unlimited, and `remaining` is null alongside it rather than
55
- 0. Read it before an expensive call rather than discovering the ceiling through
56
- a refusal.
181
+ `from` is a single address hash and `to` is an array of them. Pass an `idempotency_key`
182
+ and a retry can never send twice the response comes back with `idempotent: true` the
183
+ second time.
57
184
 
58
- The key needs the `read` or `subscription` scope.
185
+ ```ruby
186
+ res = mail.email.send(
187
+ from: { email: "receipts@yourdomain.com", name: "Acme" },
188
+ to: [{ email: "customer@example.com" }],
189
+ reply_to: { email: "support@yourdomain.com" },
190
+ subject: "Your receipt",
191
+ html: "<p>Thanks for your order.</p>",
192
+ text: "Thanks for your order.",
193
+ tags: ["receipt"],
194
+ metadata: { order_id: "ord-1041" },
195
+ idempotency_key: "ord-1041-receipt"
196
+ )
197
+
198
+ res["message_id"] # "msg-…"
199
+ ```
200
+
201
+ ### List and create contacts
202
+
203
+ Responses are enveloped. `list` returns `{"success", "data", "pagination"}` and takes
204
+ `page:`/`limit:` keywords rather than a params hash; `create` returns
205
+ `{"success", "data"}` with the contact under `data`.
59
206
 
60
207
  ```ruby
61
- plan = mail.plan.get
208
+ page = mail.contacts.list(page: 1, limit: 50)
209
+ puts "#{page['data'].length} of #{page['pagination']['total']}"
210
+
211
+ created = mail.contacts.create(
212
+ email: "new@example.com",
213
+ firstName: "Ada",
214
+ lastName: "Lovelace",
215
+ tags: ["beta"],
216
+ customFields: { plan: "pro" }
217
+ )
218
+
219
+ puts created["data"]["id"]
220
+ ```
221
+
222
+ `get` and `delete` take the contact id, which the route reads from the query string
223
+ rather than a path segment. `update` is different again: it identifies the contact by
224
+ **email address**, not by id.
225
+
226
+ ```ruby
227
+ mail.contacts.update("ada@example.com", status: "unsubscribed")
228
+ ```
229
+
230
+ ### Bulk import contacts
231
+
232
+ The method is `import_contacts` — `import` is not defined. Counts come back under
233
+ `summary`, and `errors` is a separate list of messages.
234
+
235
+ ```ruby
236
+ imported = mail.contacts.import_contacts(
237
+ contacts: [
238
+ { email: "a@example.com", firstName: "A" },
239
+ { email: "b@example.com", firstName: "B" }
240
+ ],
241
+ updateExisting: true
242
+ )
62
243
 
244
+ imported["summary"] # {"imported"=>…, "updated"=>…, "skipped"=>…, "errors"=>…}
245
+ imported["errors"] # array of strings
246
+ ```
247
+
248
+ ### Create and send a campaign
249
+
250
+ Campaigns take `fromName` and `fromEmail` as separate fields — there is no `from` hash
251
+ here, unlike `email.send`. `campaigns.send(id)` queues the campaign and reports it as
252
+ `scheduled`.
253
+
254
+ ```ruby
255
+ campaign = mail.campaigns.create(
256
+ name: "March launch",
257
+ subject: "We just shipped",
258
+ fromName: "Ada at Acme",
259
+ fromEmail: "hello@yourdomain.com",
260
+ replyTo: "support@yourdomain.com",
261
+ bodyHtml: "<h1>It's live</h1>",
262
+ segmentId: "seg-123"
263
+ )
264
+
265
+ queued = mail.campaigns.send(campaign["data"]["id"])
266
+ puts "#{queued['campaignId']} #{queued['status']}" # … scheduled
267
+ ```
268
+
269
+ Only `draft`, `scheduled` or `paused` campaigns can be updated, and only `draft`
270
+ campaigns can be deleted.
271
+
272
+ ### Validate an address
273
+
274
+ Each call spends a credit, and the response tells you what is left.
275
+
276
+ ```ruby
277
+ check = mail.validate.email("someone@example.com")
278
+
279
+ check["data"]["is_valid"] # true / false
280
+ check["data"]["score"] # 0–1 confidence
281
+ check["data"]["checks"] # {"syntax"=>…, "mx"=>…, "smtp"=>…}
282
+ check["data"]["flags"]["disposable"]
283
+ check["credits"]["balance_after"] # credits remaining
284
+ ```
285
+
286
+ ### Render a template
287
+
288
+ ```ruby
289
+ rendered = mail.templates.render(
290
+ template_id: "tpl-123",
291
+ variables: { name: "Ada", plan: "Pro" }
292
+ )
293
+
294
+ rendered["data"]["subject"] # "Welcome, Ada"
295
+ rendered["data"]["html"]
296
+ ```
297
+
298
+ ### Track events and revenue
299
+
300
+ The event name field is `event_name`, and purchase totals are integer **cents** in
301
+ `total_cents`.
302
+
303
+ ```ruby
304
+ mail.track.event(
305
+ email: "customer@example.com",
306
+ event_name: "viewed_pricing",
307
+ event_data: { plan: "pro" }
308
+ )
309
+
310
+ purchase = mail.track.purchase(
311
+ email: "customer@example.com",
312
+ order_id: "ord-1041",
313
+ total_cents: 9900,
314
+ currency: "USD",
315
+ items: [{ name: "Pro annual", quantity: 1, price_cents: 9900 }]
316
+ )
317
+
318
+ purchase["attribution"] # which campaign or automation earned it
319
+ ```
320
+
321
+ ### Read analytics and manage keys
322
+
323
+ Without `campaignId` you get aggregate usage and totals for the period; with one you get
324
+ that campaign's stats and rates. `keys.list` returns the keys under **`keys`**, not
325
+ `data`, and `create` returns the raw key exactly once.
326
+
327
+ ```ruby
328
+ overall = mail.analytics.overview(startDate: "2026-04-01", endDate: "2026-04-30")
329
+ one = mail.analytics.overview(campaignId: campaign_id)
330
+
331
+ keys = mail.keys.list
332
+ keys["keys"].length
333
+
334
+ fresh = mail.keys.create(name: "CI", scopes: %w[send read])
335
+ puts fresh["key"] # shown once and never again
336
+ ```
337
+
338
+ ### Verify an inbound webhook
339
+
340
+ MisarMail signs each delivery as `HMAC-SHA256(timestamp + "." + raw_body)`, sending the
341
+ digest in `X-Misar-Signature` and the Unix timestamp in `X-Misar-Timestamp`. Verify
342
+ against the **raw** request body — re-serializing the parsed hash changes key order and
343
+ whitespace, and so changes the digest. `verify` compares in constant time, rejects
344
+ timestamps older than 300 seconds by default, and returns `false` rather than raising on
345
+ malformed input.
346
+
347
+ ```ruby
348
+ ok = MisarMail::Webhooks.verify(
349
+ payload: request.raw_post,
350
+ signature: request.headers["X-Misar-Signature"],
351
+ timestamp: request.headers["X-Misar-Timestamp"],
352
+ secret: ENV.fetch("MISARMAIL_WEBHOOK_SECRET"),
353
+ tolerance: 300
354
+ )
355
+ head :bad_request unless ok
356
+ ```
357
+
358
+ `MisarMail::Webhooks.sign(payload, timestamp, secret)` produces the same digest, which is
359
+ what you want when testing your own consumer.
360
+
361
+ ## Errors
362
+
363
+ Three classes, all under `MisarMail`:
364
+
365
+ | Class | When |
366
+ | --- | --- |
367
+ | `ApiError` | Any non-2xx API response. Carries `status` and `error_type`. |
368
+ | `NetworkError` | The request never got an answer, or every retry was spent. `status` is `0`. |
369
+ | `PlanLimitError` | The subscription behind the key does not cover the call. |
370
+
371
+ `NetworkError` and `PlanLimitError` both subclass `ApiError`, so
372
+ `rescue MisarMail::ApiError` catches everything the SDK raises.
373
+
374
+ ### Plan limits
375
+
376
+ Both a spent allowance and a feature that is not on the plan answer **`403`**, carrying
377
+ `code: "plan_limit_exceeded"`. The SDK keys on that code rather than the status, which is
378
+ why a refusal is typed correctly even though 403 is otherwise an authorization failure.
379
+ It raises `PlanLimitError` and **does not retry** — retrying cannot help until the
380
+ allowance resets or the plan changes. Read `upgrade_url` to send the user somewhere
381
+ useful.
382
+
383
+ ```ruby
63
384
  begin
64
385
  mail.campaigns.create(
65
386
  name: "Blast", subject: "We just shipped",
@@ -67,29 +388,62 @@ begin
67
388
  )
68
389
  rescue MisarMail::PlanLimitError => e
69
390
  warn "#{e.feature} exhausted on #{e.plan}: #{e.upgrade_url}"
391
+ # e.retry_after is seconds until the allowance resets, when the API says so
70
392
  end
71
393
  ```
72
394
 
395
+ `mail.plan.get` returns `plan`, `sending` (the per-day and per-month email caps), `usage`
396
+ — an array with one entry per metered feature, each carrying `used`, `limit` and
397
+ `remaining` — and `upgrade`, which is null until a quota is tight. A null `limit` means
398
+ unlimited, and `remaining` is null alongside it rather than 0. Read it before an
399
+ expensive call rather than discovering the ceiling through a refusal.
400
+
401
+ The key needs the `read` or `subscription` scope.
402
+
403
+ ```ruby
404
+ plan = mail.plan.get
405
+ p plan["sending"]
406
+ p plan["usage"]
407
+ ```
408
+
73
409
  ## Streaming
74
410
 
75
- Two endpoints stream Server-Sent Events. Both sit **outside** `/v1`, which the
76
- SDK handles for you:
411
+ Two endpoints stream Server-Sent Events. Both sit **outside** `/v1`, which the SDK
412
+ handles for you:
77
413
 
78
414
  | Method | Route |
79
415
  | --- | --- |
80
- | `streaming.generateEmail` | `POST /api/ai/generate-email/stream` |
81
- | `streaming.campaignSend` | `GET /api/campaigns/{id}/send-stream` |
416
+ | `streaming.generate_email` | `POST /api/ai/generate-email/stream` |
417
+ | `streaming.campaign_send` | `GET /api/campaigns/{id}/send-stream` |
418
+
419
+ Frames are unnamed (`data: {…}`, with no `event:` line) and the stream ends with
420
+ `data: [DONE]`, which the SDK consumes rather than handing on. Each `StreamEvent` carries
421
+ `event` (normally nil), `data` (the parsed hash, or nil when the payload was not JSON)
422
+ and `raw`. Without a block you get an `Enumerator` instead. A stream is never retried:
423
+ replaying one that failed mid-flight would duplicate whatever you had already read.
82
424
 
83
- Frames are unnamed (`data: {…}`) and the stream ends with `data: [DONE]`, which
84
- the SDK consumes rather than handing on. A stream is never retried: replaying one
85
- that failed mid-flight would duplicate whatever you had already read.
425
+ Note that streaming always talks to `https://api.misar.io/mail` unlike the other
426
+ methods it does not follow the `base_url:` you passed the client.
86
427
 
87
428
  ```ruby
88
429
  mail.streaming.generate_email(prompt: "a launch email") do |event|
89
430
  print event.data["delta"]
90
431
  end
432
+
433
+ mail.streaming.campaign_send(campaign_id) do |event|
434
+ puts event.raw
435
+ end
91
436
  ```
92
437
 
93
- ## License
438
+ ---
439
+
440
+ ## Links
441
+
442
+ - Website — https://www.misarmail.com
443
+ - App — https://mail.misar.io
444
+ - Parent — https://misar.io
445
+ - Documentation — https://docs.misar.io/mail
446
+ - Source — https://github.com/Misar-AI/misarmail-sdks
447
+ - RubyGems — https://rubygems.org/gems/misarmail
94
448
 
95
- MIT see [LICENSE](LICENSE).
449
+ MIT © [Misar AI](https://misar.io)
@@ -836,7 +836,10 @@ module MisarMail
836
836
  end
837
837
 
838
838
  def parse_response(resp, status)
839
- return {} if status == 204 || resp.body.nil? || resp.body.empty?
839
+ # Only a *successful* empty body means "no content". An error with an empty
840
+ # body — a bare 401, or anything a proxy stripped — must still raise, or the
841
+ # caller reads a failure as an empty result set.
842
+ return {} if status < 400 && (status == 204 || resp.body.nil? || resp.body.empty?)
840
843
 
841
844
  decoded = begin
842
845
  JSON.parse(resp.body)
@@ -846,6 +849,8 @@ module MisarMail
846
849
 
847
850
  if status >= 400
848
851
  msg = decoded.is_a?(Hash) ? (decoded["error"] || decoded["message"] || resp.body) : resp.body
852
+ # A stripped body leaves nothing to report; fall back to the reason phrase.
853
+ msg = resp.message.to_s if msg.nil? || msg.empty?
849
854
  raise ApiError.new(status, msg)
850
855
  end
851
856
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: misarmail
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misar AI
@@ -52,8 +52,12 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.22'
55
- description: Full-featured Ruby SDK for the MisarMail API (misarmail.com). Covers
56
- all 24 resource groups and 101 methods.
55
+ description: 'Ruby client for MisarMail''s transactional and marketing email API (misarmail.com):
56
+ send mail, run campaigns and A/B tests, manage contacts, segments, templates and
57
+ automations, verify domains and DMARC, validate addresses, track revenue and read
58
+ analytics. 33 resource groups on one client, plus retries with backoff, typed plan-limit
59
+ errors, SSE streaming, and constant-time webhook signature verification. No runtime
60
+ dependencies beyond the stdlib.'
57
61
  email:
58
62
  - hello@misar.io
59
63
  executables: []
@@ -70,12 +74,13 @@ files:
70
74
  - lib/misar_mail/core/transport.rb
71
75
  - lib/misar_mail/core/webhooks.rb
72
76
  - lib/misar_mail/errors.rb
73
- homepage: https://misarmail.com/docs/sdks/ruby
77
+ homepage: https://www.misarmail.com
74
78
  licenses:
75
79
  - MIT
76
80
  metadata:
77
- homepage_uri: https://misarmail.com/docs/sdks/ruby
78
- source_code_uri: https://github.com/Misar-AI/misarmail-sdks
81
+ homepage_uri: https://www.misarmail.com
82
+ documentation_uri: https://docs.misar.io/mail
83
+ source_code_uri: https://github.com/Misar-AI/misarmail-sdks/tree/main/ruby
79
84
  changelog_uri: https://github.com/Misar-AI/misarmail-sdks/blob/main/ruby/CHANGELOG.md
80
85
  bug_tracker_uri: https://github.com/Misar-AI/misarmail-sdks/issues
81
86
  post_install_message:
@@ -96,6 +101,6 @@ requirements: []
96
101
  rubygems_version: 3.5.22
97
102
  signing_key:
98
103
  specification_version: 4
99
- summary: Official Ruby SDK for MisarMail transactional email, campaigns, leads,
100
- CRM
104
+ summary: 'Ruby client for MisarMail: transactional send, campaigns, contacts, templates,
105
+ automations, deliverability and analytics in one API'
101
106
  test_files: []