unsent 1.0.0 → 1.0.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +273 -2
  4. data/lib/unsent/analytics.rb +30 -0
  5. data/lib/unsent/api_keys.rb +21 -0
  6. data/lib/unsent/campaigns.rb +4 -0
  7. data/lib/unsent/client.rb +24 -12
  8. data/lib/unsent/contact_books.rb +29 -0
  9. data/lib/unsent/contacts.rb +11 -0
  10. data/lib/unsent/emails.rb +59 -6
  11. data/lib/unsent/errors.rb +2 -0
  12. data/lib/unsent/models/add_suppression_request.rb +223 -0
  13. data/lib/unsent/models/create_api_key_request.rb +218 -0
  14. data/lib/unsent/models/create_campaign200_response.rb +750 -0
  15. data/lib/unsent/models/create_campaign_request.rb +426 -0
  16. data/lib/unsent/models/create_campaign_request_reply_to.rb +103 -0
  17. data/lib/unsent/models/create_contact200_response.rb +147 -0
  18. data/lib/unsent/models/create_contact_book200_response.rb +304 -0
  19. data/lib/unsent/models/create_contact_book_request.rb +193 -0
  20. data/lib/unsent/models/create_contact_request.rb +202 -0
  21. data/lib/unsent/models/create_domain_request.rb +190 -0
  22. data/lib/unsent/models/create_template200_response.rb +164 -0
  23. data/lib/unsent/models/create_template_request.rb +226 -0
  24. data/lib/unsent/models/delete_contact_book200_response.rb +164 -0
  25. data/lib/unsent/models/get_api_keys200_response_inner.rb +278 -0
  26. data/lib/unsent/models/get_campaigns200_response_inner.rb +296 -0
  27. data/lib/unsent/models/get_contact_book200_response.rb +330 -0
  28. data/lib/unsent/models/get_contact_book200_response_details.rb +218 -0
  29. data/lib/unsent/models/get_domains200_response_inner.rb +482 -0
  30. data/lib/unsent/models/get_domains200_response_inner_dns_records_inner.rb +318 -0
  31. data/lib/unsent/models/get_health200_response.rb +216 -0
  32. data/lib/unsent/models/get_templates200_response_inner.rb +314 -0
  33. data/lib/unsent/models/list_emails_domain_id_parameter.rb +103 -0
  34. data/lib/unsent/models/schedule_campaign_request.rb +185 -0
  35. data/lib/unsent/models/send_email_request.rb +378 -0
  36. data/lib/unsent/models/send_email_request_to.rb +103 -0
  37. data/lib/unsent/models/update_contact_book200_response.rb +190 -0
  38. data/lib/unsent/models/update_contact_book_request.rb +167 -0
  39. data/lib/unsent/models/update_contact_request.rb +176 -0
  40. data/lib/unsent/models/update_template_request.rb +174 -0
  41. data/lib/unsent/settings.rb +13 -0
  42. data/lib/unsent/suppressions.rb +28 -0
  43. data/lib/unsent/templates.rb +29 -0
  44. data/lib/unsent/version.rb +1 -1
  45. data/lib/unsent/webhooks.rb +25 -0
  46. data/lib/unsent.rb +7 -0
  47. metadata +38 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95a31fbac0136fab785027f7812e7b3122ea28f79dcda16dd8bcf30782d6a788
4
- data.tar.gz: 893a89d9dba0d1ae63f1c96fe62d30b99bee7029cda09aa8d4d88172f3875b53
3
+ metadata.gz: aa505d7132d95fc8d1d1081eacdc6dc1c152d7265775437c7841a63f0cfbab0b
4
+ data.tar.gz: d173d3f42bb1b159f2a4d59070d7f1c6db41bb8af2771c29d1808f742541394a
5
5
  SHA512:
6
- metadata.gz: ee78bacbfaa8d1cafe58412d8fef02d342bc01542e255257c5bc2e73ea1a94d9d2a64bdf89939c786fd0f6d4c30c4ce58a6749118c1181ebb41661fe99bff98c
7
- data.tar.gz: 2ea07e0707bc12b3db46e94470521d315df377f35fd6e2b060b10ca9fedb8e38619001abfc5022d78f890f7cb238a4d1f0110e30de26a98783a17c9f5a29dd04
6
+ metadata.gz: 22ae152f36d21cc5f7dda582594d1b0cb0e667fba6765e9dbf3e14f23e9cf2f65f15f4ff937a09a36303d04d0d4208c5dfd5aa31121bd6c86b759dbc6a40dffd
7
+ data.tar.gz: 12f06ce781b6c75e5cc7985bd9eaa5dd1f48754715a0115cca022ed3467de14daf716284a373d17a2ea7d8beb7cb5d661b5c8b95ecf5ef0b6be18efd2daec831
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 TODO: Write your name
3
+ Copyright (c) 2025 Sourav Ukil
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -35,7 +35,7 @@ gem install unsent
35
35
  ```ruby
36
36
  require 'unsent'
37
37
 
38
- client = Unsent::Client.new('us_xxx')
38
+ client = Unsent::Client.new('un_xxx')
39
39
  ```
40
40
 
41
41
  ### Environment Variables
@@ -109,6 +109,21 @@ data, error = client.emails.batch(emails)
109
109
  puts "Sent #{data['emails'].length} emails" if data
110
110
  ```
111
111
 
112
+ #### Idempotent Retries
113
+
114
+ To prevent duplicate emails when retrying failed requests, you can provide an idempotency key.
115
+
116
+ ```ruby
117
+ data, error = client.emails.send({
118
+ to: 'user@example.com',
119
+ from: 'no-reply@yourdomain.com',
120
+ subject: 'Welcome',
121
+ html: '<strong>Hello!</strong>'
122
+ }, {
123
+ idempotency_key: 'unique-key-123'
124
+ })
125
+ ```
126
+
112
127
  ### Managing Emails
113
128
 
114
129
  #### Get Email
@@ -252,7 +267,7 @@ end
252
267
  To handle errors as return values instead:
253
268
 
254
269
  ```ruby
255
- client = Unsent::Client.new('us_xxx', raise_on_error: false)
270
+ client = Unsent::Client.new('un_xxx', raise_on_error: false)
256
271
 
257
272
  data, error = client.emails.get('email_123')
258
273
  if error
@@ -262,6 +277,262 @@ else
262
277
  end
263
278
  ```
264
279
 
280
+ ## Email Management
281
+
282
+ ### List Emails
283
+
284
+ Retrieve a paginated list of emails with optional filters:
285
+
286
+ ```ruby
287
+ data, error = client.emails.list(
288
+ page: 1,
289
+ limit: 10,
290
+ startDate: '2024-01-01',
291
+ endDate: '2024-01-31',
292
+ domainId: 'domain_123'
293
+ )
294
+
295
+ # Support for multiple domain IDs
296
+ data, error = client.emails.list(domainId: ['domain_1', 'domain_2'])
297
+ ```
298
+
299
+ ### Email Statistics
300
+
301
+ ```ruby
302
+ # Get complaints
303
+ data, error = client.emails.get_complaints(page: 1, limit: 10)
304
+
305
+ # Get bounces
306
+ data, error = client.emails.get_bounces(page: 1, limit: 10)
307
+
308
+ # Get unsubscribes
309
+ data, error = client.emails.get_unsubscribes(page: 1, limit: 10)
310
+ ```
311
+
312
+ ## Contact Books
313
+
314
+ Organize your contacts into separate books.
315
+
316
+ ### Create Contact Book
317
+
318
+ ```ruby
319
+ data, error = client.contact_books.create(
320
+ name: 'Newsletter Subscribers',
321
+ emoji: '📧'
322
+ )
323
+ ```
324
+
325
+ ### List Contact Books
326
+
327
+ ```ruby
328
+ books, error = client.contact_books.list
329
+ books.each { |book| puts book['name'] }
330
+ ```
331
+
332
+ ### Get Contact Book
333
+
334
+ ```ruby
335
+ book, error = client.contact_books.get('book_123')
336
+ ```
337
+
338
+ ### Update Contact Book
339
+
340
+ ```ruby
341
+ data, error = client.contact_books.update('book_123', name: 'Updated Name')
342
+ ```
343
+
344
+ ### Delete Contact Book
345
+
346
+ ```ruby
347
+ data, error = client.contact_books.delete('book_123')
348
+ ```
349
+
350
+ ## Contacts
351
+
352
+ ### List Contacts
353
+
354
+ ```ruby
355
+ data, error = client.contacts.list('book_123',
356
+ page: 1,
357
+ limit: 10,
358
+ search: 'john@example.com'
359
+ )
360
+ ```
361
+
362
+ ## Campaigns
363
+
364
+ ### List Campaigns
365
+
366
+ ```ruby
367
+ campaigns, error = client.campaigns.list
368
+ ```
369
+
370
+ ## Analytics
371
+
372
+ Get insights into your email sending performance.
373
+
374
+ ### Overall Analytics
375
+
376
+ ```ruby
377
+ data, error = client.analytics.get
378
+ puts "Sent: #{data['sent']}, Delivered: #{data['delivered']}"
379
+ ```
380
+
381
+ ### Time Series Data
382
+
383
+ ```ruby
384
+ data, error = client.analytics.get_time_series(
385
+ days: 30,
386
+ domain: 'yourdomain.com'
387
+ )
388
+ ```
389
+
390
+ ### Reputation Score
391
+
392
+ ```ruby
393
+ data, error = client.analytics.get_reputation(domain: 'yourdomain.com')
394
+ puts "Reputation Score: #{data['score']}"
395
+ ```
396
+
397
+ ## Templates
398
+
399
+ Manage reusable email templates.
400
+
401
+ ### Create Template
402
+
403
+ ```ruby
404
+ data, error = client.templates.create(
405
+ name: 'Welcome Email',
406
+ subject: 'Welcome to {{companyName}}!',
407
+ html: '<h1>Welcome {{firstName}}!</h1>'
408
+ )
409
+ ```
410
+
411
+ ### List Templates
412
+
413
+ ```ruby
414
+ templates, error = client.templates.list
415
+ ```
416
+
417
+ ### Get Template
418
+
419
+ ```ruby
420
+ template, error = client.templates.get('template_123')
421
+ ```
422
+
423
+ ### Update Template
424
+
425
+ ```ruby
426
+ data, error = client.templates.update('template_123',
427
+ subject: 'Updated Subject'
428
+ )
429
+ ```
430
+
431
+ ### Delete Template
432
+
433
+ ```ruby
434
+ data, error = client.templates.delete('template_123')
435
+ ```
436
+
437
+ ## Suppressions
438
+
439
+ Manage your email suppression list.
440
+
441
+ ### List Suppressions
442
+
443
+ ```ruby
444
+ data, error = client.suppressions.list(
445
+ page: 1,
446
+ limit: 10,
447
+ reason: 'MANUAL',
448
+ search: 'user@'
449
+ )
450
+ ```
451
+
452
+ ### Add to Suppression List
453
+
454
+ ```ruby
455
+ data, error = client.suppressions.add(
456
+ email: 'blocked@example.com',
457
+ reason: 'MANUAL'
458
+ )
459
+ ```
460
+
461
+ ### Remove from Suppression List
462
+
463
+ ```ruby
464
+ data, error = client.suppressions.delete('blocked@example.com')
465
+ ```
466
+
467
+ ## API Keys
468
+
469
+ Manage your API keys programmatically.
470
+
471
+ ### List API Keys
472
+
473
+ ```ruby
474
+ keys, error = client.api_keys.list
475
+ ```
476
+
477
+ ### Create API Key
478
+
479
+ ```ruby
480
+ data, error = client.api_keys.create(
481
+ name: 'Production Key',
482
+ permission: 'SENDING'
483
+ )
484
+ puts "New key: #{data['key']}"
485
+ ```
486
+
487
+ ### Delete API Key
488
+
489
+ ```ruby
490
+ data, error = client.api_keys.delete('key_123')
491
+ ```
492
+
493
+ ## Webhooks
494
+
495
+ > **Note**: Webhooks are currently a future feature and are documented here for reference.
496
+
497
+ Configure webhooks to receive real-time notifications.
498
+
499
+ ### List Webhooks
500
+
501
+ ```ruby
502
+ webhooks, error = client.webhooks.list
503
+ ```
504
+
505
+ ### Create Webhook
506
+
507
+ ```ruby
508
+ data, error = client.webhooks.create(
509
+ url: 'https://yourdomain.com/webhooks',
510
+ events: ['email.sent', 'email.delivered', 'email.bounced']
511
+ )
512
+ ```
513
+
514
+ ### Update Webhook
515
+
516
+ ```ruby
517
+ data, error = client.webhooks.update('webhook_123',
518
+ url: 'https://yourdomain.com/updated-webhook'
519
+ )
520
+ ```
521
+
522
+ ### Delete Webhook
523
+
524
+ ```ruby
525
+ data, error = client.webhooks.delete('webhook_123')
526
+ ```
527
+
528
+ ## Settings
529
+
530
+ Retrieve account settings.
531
+
532
+ ```ruby
533
+ settings, error = client.settings.get
534
+ ```
535
+
265
536
  ## Development
266
537
 
267
538
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unsent
4
+ class Analytics
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def get
10
+ @client.get('/analytics')
11
+ end
12
+
13
+ def get_time_series(query = {})
14
+ params = []
15
+ params << "days=#{query[:days]}" if query[:days]
16
+ params << "domain=#{query[:domain]}" if query[:domain]
17
+
18
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
19
+ @client.get("/analytics/time-series#{query_string}")
20
+ end
21
+
22
+ def get_reputation(query = {})
23
+ params = []
24
+ params << "domain=#{query[:domain]}" if query[:domain]
25
+
26
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
27
+ @client.get("/analytics/reputation#{query_string}")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unsent
4
+ class ApiKeys
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def list
10
+ @client.get('/api-keys')
11
+ end
12
+
13
+ def create(payload)
14
+ @client.post('/api-keys', payload)
15
+ end
16
+
17
+ def delete(id)
18
+ @client.delete("/api-keys/#{id}")
19
+ end
20
+ end
21
+ end
@@ -6,6 +6,10 @@ module Unsent
6
6
  @client = client
7
7
  end
8
8
 
9
+ def list
10
+ @client.get('/campaigns')
11
+ end
12
+
9
13
  def create(payload)
10
14
  @client.post("/campaigns", payload)
11
15
  end
data/lib/unsent/client.rb CHANGED
@@ -9,7 +9,7 @@ module Unsent
9
9
  DEFAULT_BASE_URL = "https://api.unsent.dev"
10
10
 
11
11
  attr_reader :key, :url, :raise_on_error
12
- attr_accessor :emails, :contacts, :campaigns, :domains
12
+ attr_accessor :emails, :contacts, :campaigns, :domains, :analytics, :api_keys, :contact_books, :settings, :suppressions, :templates, :webhooks
13
13
 
14
14
  def initialize(key = nil, url: nil, raise_on_error: true)
15
15
  @key = key || ENV["UNSENT_API_KEY"] || ENV["UNSENT_API_KEY"]
@@ -24,9 +24,16 @@ module Unsent
24
24
  @contacts = Contacts.new(self)
25
25
  @campaigns = Campaigns.new(self)
26
26
  @domains = Domains.new(self)
27
+ @analytics = Analytics.new(self)
28
+ @api_keys = ApiKeys.new(self)
29
+ @contact_books = ContactBooks.new(self)
30
+ @settings = Settings.new(self)
31
+ @suppressions = Suppressions.new(self)
32
+ @templates = Templates.new(self)
33
+ @webhooks = Webhooks.new(self)
27
34
  end
28
35
 
29
- def request(method, path, body = nil)
36
+ def request(method, path, body = nil, headers = {})
30
37
  uri = URI("#{@url}#{path}")
31
38
  http = Net::HTTP.new(uri.host, uri.port)
32
39
  http.use_ssl = uri.scheme == "https"
@@ -42,6 +49,11 @@ module Unsent
42
49
 
43
50
  request["Authorization"] = "Bearer #{@key}"
44
51
  request["Content-Type"] = "application/json"
52
+
53
+ headers.each do |key, value|
54
+ request[key] = value
55
+ end
56
+
45
57
  request.body = body.to_json if body
46
58
 
47
59
  response = http.request(request)
@@ -67,24 +79,24 @@ module Unsent
67
79
  end
68
80
  end
69
81
 
70
- def post(path, body)
71
- request("POST", path, body)
82
+ def post(path, body, headers = {})
83
+ request("POST", path, body, headers)
72
84
  end
73
85
 
74
- def get(path)
75
- request("GET", path)
86
+ def get(path, headers = {})
87
+ request("GET", path, nil, headers)
76
88
  end
77
89
 
78
- def put(path, body)
79
- request("PUT", path, body)
90
+ def put(path, body, headers = {})
91
+ request("PUT", path, body, headers)
80
92
  end
81
93
 
82
- def patch(path, body)
83
- request("PATCH", path, body)
94
+ def patch(path, body, headers = {})
95
+ request("PATCH", path, body, headers)
84
96
  end
85
97
 
86
- def delete(path, body = nil)
87
- request("DELETE", path, body)
98
+ def delete(path, body = nil, headers = {})
99
+ request("DELETE", path, body, headers)
88
100
  end
89
101
  end
90
102
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unsent
4
+ class ContactBooks
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def list
10
+ @client.get('/contactBooks')
11
+ end
12
+
13
+ def create(payload)
14
+ @client.post('/contactBooks', payload)
15
+ end
16
+
17
+ def get(id)
18
+ @client.get("/contactBooks/#{id}")
19
+ end
20
+
21
+ def update(id, payload)
22
+ @client.patch("/contactBooks/#{id}", payload)
23
+ end
24
+
25
+ def delete(id)
26
+ @client.delete("/contactBooks/#{id}")
27
+ end
28
+ end
29
+ end
@@ -6,6 +6,17 @@ module Unsent
6
6
  @client = client
7
7
  end
8
8
 
9
+ def list(book_id, query = {})
10
+ params = []
11
+ params << "emails=#{query[:emails]}" if query[:emails]
12
+ params << "page=#{query[:page]}" if query[:page]
13
+ params << "limit=#{query[:limit]}" if query[:limit]
14
+ params << "ids=#{query[:ids]}" if query[:ids]
15
+
16
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
17
+ @client.get("/contactBooks/#{book_id}/contacts#{query_string}")
18
+ end
19
+
9
20
  def create(book_id, payload)
10
21
  @client.post("/contactBooks/#{book_id}/contacts", payload)
11
22
  end
data/lib/unsent/emails.rb CHANGED
@@ -6,11 +6,11 @@ module Unsent
6
6
  @client = client
7
7
  end
8
8
 
9
- def send(payload)
10
- create(payload)
9
+ def send(payload, options = {})
10
+ create(payload, options)
11
11
  end
12
12
 
13
- def create(payload)
13
+ def create(payload, options = {})
14
14
  # Normalize from_ to from
15
15
  payload = payload.dup
16
16
  payload[:from] = payload.delete(:from_) if payload.key?(:from_) && !payload.key?(:from)
@@ -20,10 +20,13 @@ module Unsent
20
20
  payload[:scheduledAt] = payload[:scheduledAt].iso8601
21
21
  end
22
22
 
23
- @client.post("/emails", payload)
23
+ headers = {}
24
+ headers["Idempotency-Key"] = options[:idempotency_key] if options[:idempotency_key]
25
+
26
+ @client.post("/emails", payload, headers)
24
27
  end
25
28
 
26
- def batch(emails)
29
+ def batch(emails, options = {})
27
30
  items = emails.map do |email|
28
31
  email = email.dup
29
32
  email[:from] = email.delete(:from_) if email.key?(:from_) && !email.key?(:from)
@@ -31,7 +34,10 @@ module Unsent
31
34
  email
32
35
  end
33
36
 
34
- @client.post("/emails/batch", items)
37
+ headers = {}
38
+ headers["Idempotency-Key"] = options[:idempotency_key] if options[:idempotency_key]
39
+
40
+ @client.post("/emails/batch", items, headers)
35
41
  end
36
42
 
37
43
  def get(email_id)
@@ -47,5 +53,52 @@ module Unsent
47
53
  def cancel(email_id)
48
54
  @client.post("/emails/#{email_id}/cancel", {})
49
55
  end
56
+
57
+ def list(query = {})
58
+ params = []
59
+ params << "page=#{query[:page]}" if query[:page]
60
+ params << "limit=#{query[:limit]}" if query[:limit]
61
+ params << "startDate=#{query[:startDate]}" if query[:startDate]
62
+ params << "endDate=#{query[:endDate]}" if query[:endDate]
63
+
64
+ # Handle domainId as both string and array
65
+ if query[:domainId]
66
+ if query[:domainId].is_a?(Array)
67
+ query[:domainId].each { |id| params << "domainId=#{id}" }
68
+ else
69
+ params << "domainId=#{query[:domainId]}"
70
+ end
71
+ end
72
+
73
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
74
+ @client.get("/emails#{query_string}")
75
+ end
76
+
77
+ def get_complaints(query = {})
78
+ params = []
79
+ params << "page=#{query[:page]}" if query[:page]
80
+ params << "limit=#{query[:limit]}" if query[:limit]
81
+
82
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
83
+ @client.get("/emails/complaints#{query_string}")
84
+ end
85
+
86
+ def get_bounces(query = {})
87
+ params = []
88
+ params << "page=#{query[:page]}" if query[:page]
89
+ params << "limit=#{query[:limit]}" if query[:limit]
90
+
91
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
92
+ @client.get("/emails/bounces#{query_string}")
93
+ end
94
+
95
+ def get_unsubscribes(query = {})
96
+ params = []
97
+ params << "page=#{query[:page]}" if query[:page]
98
+ params << "limit=#{query[:limit]}" if query[:limit]
99
+
100
+ query_string = params.empty? ? '' : "?#{params.join('&')}"
101
+ @client.get("/emails/unsubscribes#{query_string}")
102
+ end
50
103
  end
51
104
  end
data/lib/unsent/errors.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unsent
4
+ class Error < StandardError; end
5
+
4
6
  class HTTPError < Error
5
7
  attr_reader :status_code, :error, :method, :path
6
8