mailkube 1.2.1 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afd85d0c480e7cff16fd2df6344b221acf946f644e97e7f9d7b33f2472ac5c51
4
- data.tar.gz: 32f66b35b8f0e9631ca737ffcc1c51b6e1b98be6746f9a1d4019c623ce24e9ff
3
+ metadata.gz: c5efd02b82c63ffe38f919d2fa9a5141aa5c7e10a893ddc33ecdf6227d0eb379
4
+ data.tar.gz: 6cafb691b11923b959de7be667353c0f410183fa72dcb91fa538a40f5ad2eda9
5
5
  SHA512:
6
- metadata.gz: e7d1a7c1e83031bbd5fca5533ecff6635361726c401b20112e856d3b62ca62211764c3498501a36d9907a6de7a2b9d0d0304d0002a725f4f4c8c013c6e3a35d0
7
- data.tar.gz: 9c7ecae8c33e302db1a539d4fbc88ec46bd1b3edf60cda62820e7b4d3ae8d499d67f116736bec04a6445f50692dc3b7a76d108b2e209e9faa983eac9890a1a17
6
+ metadata.gz: b2b0bfab25b6a83378b6a943c1493af5ad98fd7046eb4baec3428bb93b4fbf536c376c737058baed3d0cf9a56c722e74809e79c306ee4dcf67d0b774d9b54640
7
+ data.tar.gz: c3d57c7e69b2ed05e6ab5472242d5309df53a4e0a33d83f0a473f0bf1f9164abca2c83e9fc4ed6eee936bf3440667adb373a8c8948b766c99ff81b4df9b227eb
data/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  The official Ruby SDK for [mailkube](https://mailkube.com).
9
9
 
10
+ Full product and API documentation: [docs.mailkube.com/sdks/ruby](https://docs.mailkube.com/sdks/ruby).
11
+
10
12
  Requires Ruby 3.4 or newer.
11
13
 
12
14
  ## Install
@@ -149,6 +151,28 @@ client.scheduled_emails.batches.cancel("welcome-2026-08")
149
151
  An unknown batch is a no-op reporting `canceled_count: 0` rather than a 404, so a count of zero is
150
152
  not a failure.
151
153
 
154
+ ## Your API key
155
+
156
+ `client.credential.info` describes the key the client authenticates with: its domain,
157
+ organization, source-IP allowlist, plan and send-quota usage.
158
+
159
+ ```ruby
160
+ key = client.credential.info
161
+ puts key.key_id, key.domain.name, key.organization.alias
162
+
163
+ key.plan&.quotas&.each do |quota|
164
+ puts "#{quota.name}: #{quota.used} of #{quota.limit} in #{quota.period}"
165
+ end
166
+ ```
167
+
168
+ The key is checked exactly as a send checks it, so a key that cannot send raises
169
+ `Mailkube::AuthenticationError` (`invalid_api_key`): calling `info` is how to verify a key. The call
170
+ counts as a use of the key, and plan and quota figures can be up to 30 seconds old. `plan` is nil
171
+ when the organization has no subscription, and a plan without a daily limit lists
172
+ `emails_per_month` only. `remaining` counts recipients, not messages.
173
+
174
+ The client keeps the key it was constructed with. To use another key, construct another client.
175
+
152
176
  ## Errors
153
177
 
154
178
  Every error descends from `Mailkube::Error`, so rescue the category you care about:
@@ -171,7 +195,8 @@ on the response line, which is what makes a failure traceable from your own logs
171
195
  Categories: `BadRequestError` (400), `AuthenticationError` (403), `NotFoundError` (404),
172
196
  `ConflictError` (409), `InvalidRequestError` (422), `RateLimitError` (429), `ServerError` (5xx),
173
197
  and `APIError` for anything else, which is also the parent of all of them. A transport failure
174
- raises `ConnectionError` and is deliberately **not** an API error.
198
+ raises `ConnectionError` and is deliberately **not** an API error. Neither is a 2xx response whose
199
+ body is not the shape the call expects: that raises the base `Mailkube::Error`.
175
200
 
176
201
  `Mailkube::ErrorName` lists the documented `name` values for discoverability. It is **not** a
177
202
  closed set: `error_name` stays a plain String, so a value this release has never heard of is
@@ -295,7 +320,7 @@ response body. If you want pooling, inject an adapter that pools — and make it
295
320
  These are covered by semantic versioning:
296
321
 
297
322
  - `Mailkube.new`, `Mailkube.enable_logging`
298
- - `client.emails`, `client.scheduled_emails` and their verbs
323
+ - `client.emails`, `client.scheduled_emails`, `client.credential` and their verbs
299
324
  - the response models and `Mailkube::Attachment` / `Mailkube::Tag`
300
325
  - the error hierarchy and `Mailkube::ErrorName`
301
326
  - `Mailkube::Webhooks` and everything under `Mailkube::Events`
@@ -319,10 +344,14 @@ Runnable scripts in [`examples/`](examples/):
319
344
  - [`send_with_tags.rb`](examples/send_with_tags.rb) — tag a send for filtering and reporting
320
345
  - [`send_with_template.rb`](examples/send_with_template.rb) — send from a saved template
321
346
  - [`schedule_send.rb`](examples/schedule_send.rb) — schedule a send, then inspect the ack
347
+ - [`list_scheduled_emails.rb`](examples/list_scheduled_emails.rb) — one page for the metadata,
348
+ then the lazy walk, read-only
322
349
  - [`manage_scheduled_emails.rb`](examples/manage_scheduled_emails.rb) — list, paginate, retrieve,
323
350
  reschedule and cancel
324
351
  - [`schedule_batch.rb`](examples/schedule_batch.rb) — schedule a batch, then move or cancel it as
325
352
  a unit
353
+ - [`credential_info.rb`](examples/credential_info.rb) — describe the calling key, its plan and quota
354
+ usage
326
355
  - [`webhook_receiver_sinatra.rb`](examples/webhook_receiver_sinatra.rb) — verify and dispatch
327
356
  webhooks in Sinatra
328
357
  - [`webhook_receiver_rails.rb`](examples/webhook_receiver_rails.rb) — the same in Rails, including
@@ -26,6 +26,8 @@ module Mailkube
26
26
  attr_reader :emails
27
27
  # @return [Resources::ScheduledEmails] the scheduled-emails namespace.
28
28
  attr_reader :scheduled_emails
29
+ # @return [Resources::Credential] the namespace for the API key this client authenticates with.
30
+ attr_reader :credential
29
31
 
30
32
  # Create a client, resolving configuration from the arguments then the environment.
31
33
  #
@@ -49,6 +51,7 @@ module Mailkube
49
51
  @config = config
50
52
  @emails = Resources::Emails.new(transport)
51
53
  @scheduled_emails = Resources::ScheduledEmails.new(transport)
54
+ @credential = Resources::Credential.new(transport)
52
55
  freeze
53
56
  end
54
57
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Resources
5
+ # The client's own credential, reached as `client.credential`.
6
+ #
7
+ # It describes the API key the client was constructed with and holds no setter: a client takes
8
+ # its key at construction and keeps it, which is what makes one instance safe to share across
9
+ # threads. A second key is a second client.
10
+ class Credential
11
+ # @param transport [#request_json] the transport performing this resource's requests.
12
+ def initialize(transport)
13
+ @transport = transport
14
+ freeze
15
+ end
16
+
17
+ # Describe the API key this client authenticates with: its domain, organization, source-IP
18
+ # allowlist, plan and send-quota usage.
19
+ #
20
+ # The key is checked exactly as a send checks it, so a key that cannot send raises
21
+ # {AuthenticationError} (`invalid_api_key`), which also makes this the way to verify a key.
22
+ # The call counts as a use of the key. Plan and quota figures can be up to 30 seconds old.
23
+ #
24
+ # @return [APIKey] the calling API key.
25
+ # @raise [APIError] on any non-2xx response.
26
+ # @raise [ConnectionError] on a transport failure or timeout.
27
+ def info
28
+ APIKey.from(@transport.request_json(RequestSpec.new(path: "api-keys/me", method: "GET")))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -80,12 +80,14 @@ module Mailkube
80
80
  # @param spec [RequestSpec] the request to perform.
81
81
  # @return [Email] the accepted-send result.
82
82
  # @raise [APIError] on any non-2xx response.
83
+ # @raise [Error] on a 2xx body without an `id`: a body of the wrong shape is an SDK-level
84
+ # failure, not an API error.
83
85
  # @raise [ConnectionError] on a transport failure or timeout.
84
86
  def send_email(spec)
85
87
  response = perform(spec)
86
88
  payload = decode(response.body)
87
89
  id = payload["id"]
88
- raise APIError.new("expected a JSON body with an 'id'", status_code: response.status) unless id.is_a?(String)
90
+ raise Error, "expected a JSON body with an 'id' from a #{response.status} response" unless id.is_a?(String)
89
91
 
90
92
  Email.new(
91
93
  id: id,
@@ -111,12 +113,14 @@ module Mailkube
111
113
  #
112
114
  # @param spec [RequestSpec] the request to perform.
113
115
  # @return [Hash{String => Object}] the decoded 2xx body.
114
- # @raise [APIError] on any non-2xx response, or a 2xx body that is not a JSON object.
116
+ # @raise [APIError] on any non-2xx response.
117
+ # @raise [Error] on a 2xx body that is not a JSON object: a body of the wrong shape is an
118
+ # SDK-level failure, not an API error.
115
119
  # @raise [ConnectionError] on a transport failure or timeout.
116
120
  def request_json(spec)
117
121
  response = perform(spec)
118
122
  payload = decode_object(response.body)
119
- raise APIError.new("expected a JSON object body", status_code: response.status) if payload.nil?
123
+ raise Error, "expected a JSON object body from a #{response.status} response" if payload.nil?
120
124
 
121
125
  payload
122
126
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ # The API key the client authenticates with, as it describes itself.
5
+ #
6
+ # Returned by {Resources::Credential#info}. Timestamps stay the verbatim ISO-8601 strings the
7
+ # server sent; call `Time.iso8601` yourself if you want objects. Plan and quota figures can be up
8
+ # to 30 seconds old.
9
+ class APIKey < Data.define(:object, :key_id, :name, :description, :created_at, :last_auth_at,
10
+ :allowed_source_ips, :domain, :organization, :plan)
11
+ # @param object [String] the resource discriminator, always `api_key`.
12
+ # @param key_id [String] the key's public identifier.
13
+ # @param name [String] the key's name.
14
+ # @param description [String, nil] the key's description.
15
+ # @param created_at [String] when the key was created.
16
+ # @param last_auth_at [String, nil] the key's last successful authentication, this call included.
17
+ # @param allowed_source_ips [Array<String>] the CIDR networks the key may be used from; empty
18
+ # means any address.
19
+ # @param domain [APIKeyDomain] the sending domain the key belongs to.
20
+ # @param organization [APIKeyOrganization] the organization owning the domain.
21
+ # @param plan [APIKeyPlan, nil] the organization's plan, nil when it has no subscription.
22
+ def initialize(key_id:, object: "api_key", name: "", description: nil, created_at: "", last_auth_at: nil,
23
+ allowed_source_ips: [], domain: APIKeyDomain.new, organization: APIKeyOrganization.new,
24
+ plan: nil)
25
+ super
26
+ end
27
+
28
+ # `plan` is the one field the API sends as null, meaning "no subscription", so it is decoded
29
+ # only when present: an empty plan built from defaults would look like a real one.
30
+ #
31
+ # @param payload [Hash{String => Object}] the decoded `api_key` object.
32
+ # @return [APIKey] the model.
33
+ def self.from(payload)
34
+ new(key_id: payload["key_id"], object: payload["object"] || "api_key", name: payload["name"] || "",
35
+ description: payload["description"], created_at: payload["created_at"] || "",
36
+ last_auth_at: payload["last_auth_at"], allowed_source_ips: payload["allowed_source_ips"] || [],
37
+ domain: APIKeyDomain.from(payload["domain"]), organization: APIKeyOrganization.from(payload["organization"]),
38
+ plan: payload["plan"] && APIKeyPlan.from(payload["plan"]))
39
+ end
40
+ end
41
+
42
+ # The sending domain an API key belongs to.
43
+ class APIKeyDomain < Data.define(:name)
44
+ # @param name [String] the domain name, e.g. `example.com`.
45
+ def initialize(name: "") = super
46
+
47
+ # @param payload [Hash{String => Object}, nil] the decoded `domain` block.
48
+ # @return [APIKeyDomain] the model.
49
+ def self.from(payload) = new(name: (payload || {})["name"] || "")
50
+ end
51
+
52
+ # The organization owning an API key's domain.
53
+ class APIKeyOrganization < Data.define(:name, :alias)
54
+ # @param name [String] the organization's display name.
55
+ # @param alias [String] the organization's short alias.
56
+ def initialize(name: "", alias: "") = super
57
+
58
+ # @param payload [Hash{String => Object}, nil] the decoded `organization` block.
59
+ # @return [APIKeyOrganization] the model.
60
+ def self.from(payload)
61
+ block = payload || {}
62
+ new(name: block["name"] || "", alias: block["alias"] || "")
63
+ end
64
+ end
65
+
66
+ # The organization's plan, its status and its send quotas.
67
+ #
68
+ # `status` is a server-controlled string, deliberately not an enum. A plan without a daily limit
69
+ # lists the monthly quota only: an absent quota is a missing limit, never a limit of zero.
70
+ class APIKeyPlan < Data.define(:code, :name, :status, :quotas)
71
+ # @param code [String] the plan's UUID.
72
+ # @param name [String] the plan's name.
73
+ # @param status [String] the subscription status, e.g. `active`.
74
+ # @param quotas [Array<APIKeyQuota>] the send quotas the plan carries.
75
+ def initialize(code: "", name: "", status: "", quotas: []) = super
76
+
77
+ # @param payload [Hash{String => Object}] the decoded `plan` block.
78
+ # @return [APIKeyPlan] the model.
79
+ def self.from(payload)
80
+ new(code: payload["code"] || "", name: payload["name"] || "", status: payload["status"] || "",
81
+ quotas: (payload["quotas"] || []).map { |quota| APIKeyQuota.from(quota) })
82
+ end
83
+ end
84
+
85
+ # One send quota of the plan: its limit and what the current period has used, in recipients.
86
+ class APIKeyQuota < Data.define(:name, :limit, :used, :remaining, :period)
87
+ # @param name [String] which quota, e.g. `emails_per_day` or `emails_per_month`; a
88
+ # server-controlled string, deliberately not an enum.
89
+ # @param limit [Integer] the plan's limit for the period.
90
+ # @param used [Integer] recipients sent to so far in the period.
91
+ # @param remaining [Integer] recipients left in the period.
92
+ # @param period [String] the UTC day (`2026-10-19`) or month (`2026-10`) the figures count.
93
+ def initialize(name: "", limit: 0, used: 0, remaining: 0, period: "") = super
94
+
95
+ # @param payload [Hash{String => Object}] one decoded quota.
96
+ # @return [APIKeyQuota] the model.
97
+ def self.from(payload)
98
+ new(name: payload["name"] || "", limit: payload["limit"] || 0, used: payload["used"] || 0,
99
+ remaining: payload["remaining"] || 0, period: payload["period"] || "")
100
+ end
101
+ end
102
+ end
@@ -11,5 +11,5 @@ module Mailkube
11
11
  # semantic-release rewrites this line **in the release runner** just before the gem is built, and
12
12
  # commits nothing back to `main` (see `.rules/RELEASE.md`). So a checkout reports `0.0.0` and an
13
13
  # installed gem reports the real version: that is intended, not a bug to fix by hardcoding one.
14
- VERSION = "1.2.1"
14
+ VERSION = "1.3.0"
15
15
  end
data/lib/mailkube.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "mailkube/logging"
6
6
  require_relative "mailkube/types"
7
7
  require_relative "mailkube/types/scheduled_emails"
8
8
  require_relative "mailkube/types/scheduled_acks"
9
+ require_relative "mailkube/types/api_key"
9
10
  require_relative "mailkube/serialization"
10
11
  require_relative "mailkube/config"
11
12
  require_relative "mailkube/transport"
@@ -13,6 +14,7 @@ require_relative "mailkube/net_http_adapter"
13
14
  require_relative "mailkube/resources/emails"
14
15
  require_relative "mailkube/resources/scheduled_email_requests"
15
16
  require_relative "mailkube/resources/scheduled_emails"
17
+ require_relative "mailkube/resources/credential"
16
18
  require_relative "mailkube/client"
17
19
  require_relative "mailkube/events/node"
18
20
  require_relative "mailkube/events/contexts"
@@ -2,6 +2,7 @@ module Mailkube
2
2
  class Client
3
3
  attr_reader emails: Resources::Emails
4
4
  attr_reader scheduled_emails: Resources::ScheduledEmails
5
+ attr_reader credential: Resources::Credential
5
6
 
6
7
  def initialize: (?api_key: String?, ?base_url: String?, ?timeout: Numeric, ?http: _HttpAdapter?,
7
8
  ?user_agent_suffix: String?) -> void
@@ -0,0 +1,8 @@
1
+ module Mailkube
2
+ module Resources
3
+ class Credential
4
+ def initialize: (_JsonTransport) -> void
5
+ def info: () -> APIKey
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,57 @@
1
+ module Mailkube
2
+ # Declared the same way as every other model; see `sig/mailkube/types.rbs`.
3
+ class APIKey
4
+ attr_reader object: String
5
+ attr_reader key_id: String
6
+ attr_reader name: String
7
+ attr_reader description: String?
8
+ attr_reader created_at: String
9
+ attr_reader last_auth_at: String?
10
+ attr_reader allowed_source_ips: Array[String]
11
+ attr_reader domain: APIKeyDomain
12
+ attr_reader organization: APIKeyOrganization
13
+ attr_reader plan: APIKeyPlan?
14
+
15
+ def initialize: (key_id: String, ?object: String, ?name: String, ?description: String?,
16
+ ?created_at: String, ?last_auth_at: String?, ?allowed_source_ips: Array[String],
17
+ ?domain: APIKeyDomain, ?organization: APIKeyOrganization, ?plan: APIKeyPlan?) -> void
18
+ def self.from: (Hash[String, untyped]) -> APIKey
19
+ end
20
+
21
+ class APIKeyDomain
22
+ attr_reader name: String
23
+
24
+ def initialize: (?name: String) -> void
25
+ def self.from: (Hash[String, untyped]?) -> APIKeyDomain
26
+ end
27
+
28
+ class APIKeyOrganization
29
+ attr_reader name: String
30
+ # Declared as `def` because `alias` is a keyword, as `PageSteps#next` is.
31
+ def alias: () -> String
32
+
33
+ def initialize: (?name: String, ?alias: String) -> void
34
+ def self.from: (Hash[String, untyped]?) -> APIKeyOrganization
35
+ end
36
+
37
+ class APIKeyPlan
38
+ attr_reader code: String
39
+ attr_reader name: String
40
+ attr_reader status: String
41
+ attr_reader quotas: Array[APIKeyQuota]
42
+
43
+ def initialize: (?code: String, ?name: String, ?status: String, ?quotas: Array[APIKeyQuota]) -> void
44
+ def self.from: (Hash[String, untyped]) -> APIKeyPlan
45
+ end
46
+
47
+ class APIKeyQuota
48
+ attr_reader name: String
49
+ attr_reader limit: Integer
50
+ attr_reader used: Integer
51
+ attr_reader remaining: Integer
52
+ attr_reader period: String
53
+
54
+ def initialize: (?name: String, ?limit: Integer, ?used: Integer, ?remaining: Integer, ?period: String) -> void
55
+ def self.from: (Hash[String, untyped]) -> APIKeyQuota
56
+ end
57
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailkube
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mail Tactic Corporation
@@ -29,12 +29,14 @@ files:
29
29
  - lib/mailkube/events/registry.rb
30
30
  - lib/mailkube/logging.rb
31
31
  - lib/mailkube/net_http_adapter.rb
32
+ - lib/mailkube/resources/credential.rb
32
33
  - lib/mailkube/resources/emails.rb
33
34
  - lib/mailkube/resources/scheduled_email_requests.rb
34
35
  - lib/mailkube/resources/scheduled_emails.rb
35
36
  - lib/mailkube/serialization.rb
36
37
  - lib/mailkube/transport.rb
37
38
  - lib/mailkube/types.rb
39
+ - lib/mailkube/types/api_key.rb
38
40
  - lib/mailkube/types/scheduled_acks.rb
39
41
  - lib/mailkube/types/scheduled_emails.rb
40
42
  - lib/mailkube/version.rb
@@ -50,12 +52,14 @@ files:
50
52
  - sig/mailkube/events/registry.rbs
51
53
  - sig/mailkube/logging.rbs
52
54
  - sig/mailkube/net_http_adapter.rbs
55
+ - sig/mailkube/resources/credential.rbs
53
56
  - sig/mailkube/resources/emails.rbs
54
57
  - sig/mailkube/resources/scheduled_email_requests.rbs
55
58
  - sig/mailkube/resources/scheduled_emails.rbs
56
59
  - sig/mailkube/serialization.rbs
57
60
  - sig/mailkube/transport.rbs
58
61
  - sig/mailkube/types.rbs
62
+ - sig/mailkube/types/api_key.rbs
59
63
  - sig/mailkube/types/scheduled_acks.rbs
60
64
  - sig/mailkube/types/scheduled_emails.rbs
61
65
  - sig/mailkube/webhooks.rbs