mailkube 1.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/NOTICE +11 -0
  4. data/README.md +323 -0
  5. data/lib/mailkube/client.rb +52 -0
  6. data/lib/mailkube/config.rb +89 -0
  7. data/lib/mailkube/errors.rb +132 -0
  8. data/lib/mailkube/events/contexts.rb +129 -0
  9. data/lib/mailkube/events/envelopes.rb +96 -0
  10. data/lib/mailkube/events/node.rb +62 -0
  11. data/lib/mailkube/events/payloads.rb +88 -0
  12. data/lib/mailkube/events/registry.rb +26 -0
  13. data/lib/mailkube/logging.rb +113 -0
  14. data/lib/mailkube/net_http_adapter.rb +123 -0
  15. data/lib/mailkube/resources/emails.rb +81 -0
  16. data/lib/mailkube/resources/scheduled_email_requests.rb +78 -0
  17. data/lib/mailkube/resources/scheduled_emails.rb +142 -0
  18. data/lib/mailkube/serialization.rb +98 -0
  19. data/lib/mailkube/transport.rb +190 -0
  20. data/lib/mailkube/types/scheduled_acks.rb +57 -0
  21. data/lib/mailkube/types/scheduled_emails.rb +96 -0
  22. data/lib/mailkube/types.rb +54 -0
  23. data/lib/mailkube/version.rb +15 -0
  24. data/lib/mailkube/webhooks.rb +152 -0
  25. data/lib/mailkube.rb +68 -0
  26. data/sig/mailkube/client.rbs +9 -0
  27. data/sig/mailkube/config.rbs +14 -0
  28. data/sig/mailkube/errors.rbs +78 -0
  29. data/sig/mailkube/events/contexts.rbs +60 -0
  30. data/sig/mailkube/events/envelopes.rbs +60 -0
  31. data/sig/mailkube/events/node.rbs +32 -0
  32. data/sig/mailkube/events/payloads.rbs +54 -0
  33. data/sig/mailkube/events/registry.rbs +6 -0
  34. data/sig/mailkube/logging.rbs +26 -0
  35. data/sig/mailkube/net_http_adapter.rbs +15 -0
  36. data/sig/mailkube/resources/emails.rbs +18 -0
  37. data/sig/mailkube/resources/scheduled_email_requests.rbs +19 -0
  38. data/sig/mailkube/resources/scheduled_emails.rbs +32 -0
  39. data/sig/mailkube/serialization.rbs +11 -0
  40. data/sig/mailkube/transport.rbs +34 -0
  41. data/sig/mailkube/types/scheduled_acks.rbs +30 -0
  42. data/sig/mailkube/types/scheduled_emails.rbs +53 -0
  43. data/sig/mailkube/types.rbs +44 -0
  44. data/sig/mailkube/webhooks.rbs +30 -0
  45. data/sig/mailkube.rbs +36 -0
  46. metadata +88 -0
@@ -0,0 +1,78 @@
1
+ module Mailkube
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ConfigurationError < Error
6
+ end
7
+
8
+ class ConnectionError < Error
9
+ end
10
+
11
+ class SignatureVerificationError < Error
12
+ end
13
+
14
+ class APIError < Error
15
+ attr_reader error_name: String
16
+ attr_reader status_code: Integer
17
+ attr_reader body: Hash[String, untyped]
18
+ attr_reader retry_after: Integer?
19
+ attr_reader request_id: String?
20
+
21
+ def initialize: (?String?, ?error_name: String, ?status_code: Integer, ?body: Hash[String, untyped],
22
+ ?retry_after: Integer?, ?request_id: String?) -> void
23
+ end
24
+
25
+ class BadRequestError < APIError
26
+ end
27
+
28
+ class AuthenticationError < APIError
29
+ end
30
+
31
+ class NotFoundError < APIError
32
+ end
33
+
34
+ class ConflictError < APIError
35
+ end
36
+
37
+ class InvalidRequestError < APIError
38
+ end
39
+
40
+ class RateLimitError < APIError
41
+ end
42
+
43
+ class ServerError < APIError
44
+ end
45
+
46
+ module ErrorName
47
+ APPLICATION_ERROR: String
48
+ BODY_CONTENT_REJECTED: String
49
+ BROWSER_NOT_ALLOWED: String
50
+ CONCURRENT_IDEMPOTENT_REQUESTS: String
51
+ FROM_DOMAIN_NOT_ALLOWED: String
52
+ INVALID_API_KEY: String
53
+ INVALID_ATTACHMENT: String
54
+ INVALID_FROM_ADDRESS: String
55
+ INVALID_IDEMPOTENCY_KEY: String
56
+ INVALID_IDEMPOTENT_REQUEST: String
57
+ INVALID_REQUEST_BODY: String
58
+ LINK_REPUTATION_BLOCKED: String
59
+ MAX_MESSAGE_SIZE_EXCEEDED: String
60
+ MAX_RECIPIENTS_EXCEEDED: String
61
+ METHOD_NOT_ALLOWED: String
62
+ MISSING_REQUIRED_FIELD: String
63
+ MISSING_REQUIRED_VARIABLE: String
64
+ MISSING_USER_AGENT: String
65
+ NOT_ACCEPTABLE: String
66
+ QUOTA_EXCEEDED: String
67
+ RATE_LIMIT_EXCEEDED: String
68
+ SCHEDULED_EMAIL_NOT_FOUND: String
69
+ SCHEDULED_EMAIL_NOT_PENDING: String
70
+ SCHEDULING_NOT_INCLUDED: String
71
+ TEMPLATE_NOT_FOUND: String
72
+ TEMPLATE_NOT_PUBLISHED: String
73
+ TOPIC_DISABLED: String
74
+ TOPIC_NOT_FOUND: String
75
+ UNSUPPORTED_MEDIA_TYPE: String
76
+ VALIDATION_ERROR: String
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ module Mailkube
2
+ module Events
3
+ class MessageContext < Node
4
+ def email_id: () -> String
5
+ def created_at: () -> String
6
+ def domain: () -> String?
7
+ def subject: () -> String?
8
+ def to: () -> Array[String]?
9
+ def from: () -> String?
10
+ # Plain hashes, not `Mailkube::Tag`: a `Data` would drop an unknown key inside a tag.
11
+ def tags: () -> Array[Hash[String, untyped]]
12
+ end
13
+
14
+ class DeliveryContext < Node
15
+ def recipient: () -> String
16
+ def timestamp: () -> String
17
+ end
18
+
19
+ class FailureContext < DeliveryContext
20
+ def code: () -> Integer
21
+ def reason: () -> String
22
+ end
23
+
24
+ class EngagementContext < Node
25
+ def ip_address: () -> String
26
+ def user_agent: () -> String
27
+ def timestamp: () -> String
28
+ end
29
+
30
+ class ClickContext < EngagementContext
31
+ def link: () -> String
32
+ end
33
+
34
+ class SuppressionContext < Node
35
+ def recipients: () -> Array[String]
36
+ def timestamp: () -> String
37
+ end
38
+
39
+ class ScheduledContext < Node
40
+ def scheduled_at: () -> String
41
+ def batch_id: () -> String?
42
+ end
43
+
44
+ class SendFailureContext < Node
45
+ def reason: () -> String
46
+ def timestamp: () -> String
47
+ end
48
+
49
+ class DomainStatusPrevious < Node
50
+ def status: () -> String
51
+ def onboarding_state: () -> String
52
+ end
53
+
54
+ class WebhookStatusPrevious < Node
55
+ def active?: () -> bool
56
+ def deleted?: () -> bool
57
+ def disabled_reason: () -> String
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ module Mailkube
2
+ module Events
3
+ # `data` is declared per concrete subclass rather than on `Event`, which is what lets
4
+ # `UnknownEvent#data` return a raw Hash without conflicting with a typed sibling.
5
+ #
6
+ # There is no `id`: the delivery id travels in the `X-Webhook-Id` header.
7
+ class Event < Node
8
+ def type: () -> String
9
+ def created_at: () -> String
10
+ end
11
+
12
+ class EmailSentEvent < Event
13
+ def data: () -> SentData
14
+ end
15
+
16
+ class EmailDeliveredEvent < Event
17
+ def data: () -> DeliveredData
18
+ end
19
+
20
+ class EmailBouncedEvent < Event
21
+ def data: () -> BouncedData
22
+ end
23
+
24
+ class EmailDeliveryDelayedEvent < Event
25
+ def data: () -> DelayedData
26
+ end
27
+
28
+ class EmailSuppressedEvent < Event
29
+ def data: () -> SuppressedData
30
+ end
31
+
32
+ class EmailScheduledEvent < Event
33
+ def data: () -> ScheduledData
34
+ end
35
+
36
+ class EmailFailedEvent < Event
37
+ def data: () -> FailedData
38
+ end
39
+
40
+ class EmailOpenedEvent < Event
41
+ def data: () -> OpenedData
42
+ end
43
+
44
+ class EmailClickedEvent < Event
45
+ def data: () -> ClickedData
46
+ end
47
+
48
+ class DomainStatusEvent < Event
49
+ def data: () -> DomainStatusData
50
+ end
51
+
52
+ class WebhookStatusEvent < Event
53
+ def data: () -> WebhookStatusData
54
+ end
55
+
56
+ class UnknownEvent < Event
57
+ def data: () -> Hash[String, untyped]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ module Mailkube
2
+ module Events
3
+ # Event models are the one place this gem does **not** use `Data.define`.
4
+ #
5
+ # The contract inverts the response-model rule for webhooks: unknown fields are preserved, not
6
+ # dropped, at every depth. A `Data` has fixed members and drops them, so the decoded hash is
7
+ # the state instead, and each subclass declares one-line readers over it.
8
+ #
9
+ # Those readers are real `def`s, so they are declared here as `def` — `attr_reader` in these
10
+ # signatures is reserved for the readers `Data.define` actually generates (see
11
+ # `sig/mailkube/types.rbs`).
12
+ class Node
13
+ @raw: Hash[String, untyped]
14
+
15
+ attr_reader raw: Hash[String, untyped]
16
+
17
+ def initialize: (Hash[String, untyped]) -> void
18
+ def []: (String) -> untyped
19
+ def to_h: () -> Hash[String, untyped]
20
+ def ==: (untyped) -> bool
21
+ def eql?: (untyped) -> bool
22
+ def hash: () -> Integer
23
+
24
+ private
25
+
26
+ # Deliberately `untyped` rather than a bounded generic: Ruby has no generics, and every
27
+ # caller is a one-line reader that already declares its own concrete return type — which is
28
+ # where the type belongs.
29
+ def block: (untyped, String) -> untyped
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ module Mailkube
2
+ module Events
3
+ class SentData < MessageContext
4
+ def sent: () -> DeliveryContext
5
+ end
6
+
7
+ class DeliveredData < MessageContext
8
+ def delivery: () -> DeliveryContext
9
+ end
10
+
11
+ class BouncedData < MessageContext
12
+ def bounce: () -> FailureContext
13
+ end
14
+
15
+ class DelayedData < MessageContext
16
+ def delay: () -> FailureContext
17
+ end
18
+
19
+ class SuppressedData < MessageContext
20
+ def suppression: () -> SuppressionContext
21
+ end
22
+
23
+ class ScheduledData < MessageContext
24
+ def scheduled: () -> ScheduledContext
25
+ end
26
+
27
+ class FailedData < MessageContext
28
+ def failed: () -> SendFailureContext
29
+ end
30
+
31
+ class OpenedData < MessageContext
32
+ def open: () -> EngagementContext
33
+ end
34
+
35
+ class ClickedData < MessageContext
36
+ def click: () -> ClickContext
37
+ end
38
+
39
+ class DomainStatusData < Node
40
+ def domain: () -> String
41
+ def status: () -> String
42
+ def onboarding_state: () -> String
43
+ def previous: () -> DomainStatusPrevious
44
+ end
45
+
46
+ class WebhookStatusData < Node
47
+ def endpoint_url: () -> String
48
+ def active?: () -> bool
49
+ def deleted?: () -> bool
50
+ def disabled_reason: () -> String
51
+ def previous: () -> WebhookStatusPrevious
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,6 @@
1
+ module Mailkube
2
+ module Events
3
+ # The catalogue. The known-type set is `REGISTRY.keys`; there is no parallel list.
4
+ REGISTRY: Hash[String, singleton(Event)]
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ module Mailkube
2
+ # Anything a caller may point SDK logging at. The seam is `#write` rather than a stdlib `Logger`
3
+ # because `logger` is a bundled gem from Ruby 4.0 and this gem has no runtime dependencies.
4
+ interface _LogDevice
5
+ def write: (String) -> untyped
6
+ end
7
+
8
+ module Logging
9
+ ENV_LEVEL: String
10
+ VERBOSE_LEVELS: Array[String]
11
+ SENSITIVE_HEADERS: Array[String]
12
+ REDACTION: String
13
+
14
+ self.@device: _LogDevice?
15
+
16
+ def self.device: () -> _LogDevice?
17
+ def self.enable!: (?device: _LogDevice) -> _LogDevice
18
+ def self.disable!: () -> nil
19
+ def self.enable_from_env: (?Hash[String, String]) -> _LogDevice?
20
+ def self.redact: (Hash[String, String]) -> Hash[String, String]
21
+ def self.request: (String, String, Hash[String, String]) -> void
22
+ def self.response: (Integer, String, ?String?) -> void
23
+ end
24
+
25
+ def self.enable_logging: (?device: _LogDevice) -> _LogDevice
26
+ end
@@ -0,0 +1,15 @@
1
+ module Mailkube
2
+ class NetHttpAdapter
3
+ METHODS: Hash[String, untyped]
4
+ TRANSPORT_ERRORS: Array[untyped]
5
+
6
+ def initialize: (?timeout: Numeric) -> void
7
+ def call: (method: String, url: String, headers: Hash[String, String], ?body: String?) -> HttpResponse
8
+
9
+ private
10
+
11
+ def parse_url: (String) -> URI::Generic
12
+ def build_request: (String, URI::Generic, Hash[String, String], String?) -> untyped
13
+ def to_response: (untyped) -> HttpResponse
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module Mailkube
2
+ module Resources
3
+ class Emails
4
+ def initialize: (_SendTransport) -> void
5
+
6
+ def send: (from: String, to: (String | Array[String]), subject: String, ?html: String?, ?text: String?,
7
+ ?cc: Array[String]?, ?bcc: Array[String]?, ?reply_to: (String | Array[String])?,
8
+ ?headers: Hash[String, String]?, ?attachments: Array[Attachment]?, ?tags: Array[Tag]?,
9
+ ?template_id: String?, ?template_version: String?, ?variables: Hash[String, untyped]?,
10
+ ?topic: String?, ?idempotency_key: String?, ?scheduled_at: (Time | String)?,
11
+ ?batch_id: String?) -> Email
12
+
13
+ private
14
+
15
+ def idempotency: (String?) -> Hash[String, String]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Mailkube
2
+ module Resources
3
+ module ScheduledEmailRequests
4
+ COLLECTION: String
5
+ BATCHES: String
6
+
7
+ def self.item: (String, String, String, ?Hash[String, untyped]?) -> RequestSpec
8
+ def self.list: (?status: (String | Array[String])?, ?batch_id: String?,
9
+ ?scheduled_at_gte: (Time | String)?, ?scheduled_at_lte: (Time | String)?,
10
+ ?page: Integer?) -> RequestSpec
11
+ def self.page: (String) -> RequestSpec
12
+ def self.get: (String) -> RequestSpec
13
+ def self.update: (String, Hash[String, untyped]) -> RequestSpec
14
+ def self.cancel: (String) -> RequestSpec
15
+ def self.batch_update: (String, Hash[String, untyped]) -> RequestSpec
16
+ def self.batch_cancel: (String) -> RequestSpec
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Mailkube
2
+ module Resources
3
+ class ScheduledEmailBatches
4
+ def initialize: (_JsonTransport) -> void
5
+ def update: (String, scheduled_at: (Time | String)) -> ScheduledEmailBatchUpdate
6
+ def cancel: (String) -> ScheduledEmailBatchCancel
7
+ end
8
+
9
+ class ScheduledEmails
10
+ attr_reader batches: ScheduledEmailBatches
11
+
12
+ def initialize: (_JsonTransport) -> void
13
+
14
+ def list: (?status: (String | Array[String])?, ?batch_id: String?,
15
+ ?scheduled_at_gte: (Time | String)?, ?scheduled_at_lte: (Time | String)?,
16
+ ?page: Integer?) -> ScheduledEmailPage
17
+
18
+ # A lazy `Enumerator`, so abandoning the walk early costs nothing.
19
+ def iter_all: (?status: (String | Array[String])?, ?batch_id: String?,
20
+ ?scheduled_at_gte: (Time | String)?, ?scheduled_at_lte: (Time | String)?,
21
+ ?page: Integer?) -> Enumerator[ScheduledEmail, void]
22
+
23
+ def get: (String) -> ScheduledEmail
24
+ def update: (String, scheduled_at: (Time | String), ?batch_id: String?) -> ScheduledEmail
25
+ def cancel: (String) -> CanceledScheduledEmail
26
+
27
+ private
28
+
29
+ def fetch_page: (RequestSpec) -> ScheduledEmailPage
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ module Mailkube
2
+ module Serialization
3
+ def self.to_iso: ((Time | String)?) -> String?
4
+ def self.query_value: (untyped) -> String
5
+ def self.query_scalar: (untyped) -> String
6
+ def self.query: (Hash[Symbol, untyped]) -> Hash[String, String]
7
+ def self.escape_segment: (String) -> String
8
+ def self.encode_attachments: (Array[Attachment]?) -> Array[Hash[String, untyped]]?
9
+ def self.encode_tags: (Array[Tag]?) -> Array[Hash[String, untyped]]?
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ module Mailkube
2
+ class RequestSpec
3
+ attr_reader path: String
4
+ attr_reader method: String
5
+ attr_reader body: Hash[String, untyped]?
6
+ attr_reader params: Hash[String, String]
7
+ attr_reader headers: Hash[String, String]
8
+
9
+ def initialize: (path: String, ?method: String, ?body: Hash[String, untyped]?,
10
+ ?params: Hash[String, String], ?headers: Hash[String, String]) -> void
11
+ end
12
+
13
+ class HttpResponse
14
+ attr_reader status: Integer
15
+ attr_reader headers: Hash[String, String]
16
+ attr_reader body: String
17
+
18
+ def initialize: (status: Integer, ?headers: Hash[String, String], ?body: String) -> void
19
+ def header: (String) -> String?
20
+ end
21
+
22
+ class Transport
23
+ def initialize: (Config, _HttpAdapter) -> void
24
+ def send_email: (RequestSpec) -> Email
25
+ def request_json: (RequestSpec) -> Hash[String, untyped]
26
+
27
+ private
28
+
29
+ def perform: (RequestSpec) -> HttpResponse
30
+ def error_for: (HttpResponse) -> APIError
31
+ def decode_object: (String) -> Hash[String, untyped]?
32
+ def decode: (String) -> Hash[String, untyped]
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module Mailkube
2
+ class CanceledScheduledEmail
3
+ attr_reader id: String
4
+ attr_reader object: String
5
+ attr_reader status: String
6
+
7
+ def initialize: (id: String, ?object: String, ?status: String) -> void
8
+ def self.from: (Hash[String, untyped]) -> CanceledScheduledEmail
9
+ end
10
+
11
+ class ScheduledEmailBatchCancel
12
+ attr_reader object: String
13
+ attr_reader batch_id: String
14
+ attr_reader canceled_count: Integer
15
+
16
+ def initialize: (?object: String, ?batch_id: String, ?canceled_count: Integer) -> void
17
+ def self.from: (Hash[String, untyped]) -> ScheduledEmailBatchCancel
18
+ end
19
+
20
+ class ScheduledEmailBatchUpdate
21
+ attr_reader object: String
22
+ attr_reader batch_id: String
23
+ attr_reader rescheduled_count: Integer
24
+ attr_reader scheduled_at: String?
25
+
26
+ def initialize: (?object: String, ?batch_id: String, ?rescheduled_count: Integer,
27
+ ?scheduled_at: String?) -> void
28
+ def self.from: (Hash[String, untyped]) -> ScheduledEmailBatchUpdate
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ module Mailkube
2
+ # Declared the same way as every other model — see `sig/mailkube/types.rbs` for why these are
3
+ # plain classes that do not name `::Data` as a superclass.
4
+ class ScheduledEmail
5
+ attr_reader id: String
6
+ attr_reader message_id: String?
7
+ attr_reader object: String
8
+ attr_reader status: String
9
+ attr_reader scheduled_at: String?
10
+ attr_reader created_at: String?
11
+ attr_reader batch_id: String?
12
+ attr_reader subject: String?
13
+ attr_reader recipients: String?
14
+ attr_reader topic: String?
15
+ attr_reader tags: Array[Hash[String, untyped]]
16
+
17
+ def initialize: (id: String, ?message_id: String?, ?object: String, ?status: String,
18
+ ?scheduled_at: String?, ?created_at: String?, ?batch_id: String?,
19
+ ?subject: String?, ?recipients: String?, ?topic: String?,
20
+ ?tags: Array[Hash[String, untyped]]) -> void
21
+ def self.from: (Hash[String, untyped]) -> ScheduledEmail
22
+ end
23
+
24
+ class PageSteps
25
+ # Declared as `def` rather than `attr_reader` only because `next` is a Ruby keyword: RBS's own
26
+ # core signatures spell this member `def next:` and use no keyword-named `attr_reader`
27
+ # anywhere. `previous` keeps the house form. Both are `Data.define` readers, and
28
+ # `MethodDefinitionMissing` is off in the Steepfile for exactly that reason.
29
+ def next: () -> String?
30
+ attr_reader previous: String?
31
+
32
+ def initialize: (?next: String?, ?previous: String?) -> void
33
+ def self.from: (Hash[String, untyped]?) -> PageSteps
34
+ end
35
+
36
+ class Pagination
37
+ attr_reader steps: PageSteps
38
+ attr_reader total_count: Integer
39
+ attr_reader current_page: Integer
40
+
41
+ def initialize: (?steps: PageSteps, ?total_count: Integer, ?current_page: Integer) -> void
42
+ def self.from: (Hash[String, untyped]?) -> Pagination
43
+ end
44
+
45
+ class ScheduledEmailPage
46
+ attr_reader pagination: Pagination
47
+ attr_reader data: Array[ScheduledEmail]
48
+
49
+ def initialize: (?pagination: Pagination, ?data: Array[ScheduledEmail]) -> void
50
+ def self.from: (Hash[String, untyped]) -> ScheduledEmailPage
51
+ def more?: () -> bool
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ module Mailkube
2
+ # Response and parameter models.
3
+ #
4
+ # Two shapes matter here, and both were arrived at by testing Steep 2.0.0 rather than guessing.
5
+ #
6
+ # In `lib/`, each model is `class X < Data.define(...)`, not `X = Data.define(...) do ... end`.
7
+ # Steep attributes the block form's methods to the enclosing module rather than to the constant,
8
+ # and then cannot resolve the generated readers from inside them.
9
+ #
10
+ # Here, each is declared as a plain `class X` and **does not name `::Data` as its superclass**.
11
+ # Inheriting `::Data` in the signature brings its own untyped `new` along, which shadows the
12
+ # keyword-argument `initialize` below, and every call site then fails with "unexpected keyword
13
+ # argument".
14
+ #
15
+ # The generated readers are declared as `attr_reader`; `Ruby::MethodDefinitionMissing` stays off
16
+ # in the Steepfile because Steep cannot see their implementations.
17
+ class Attachment
18
+ attr_reader filename: String
19
+ attr_reader content: String
20
+ attr_reader content_type: String?
21
+
22
+ def initialize: (filename: String, content: String, ?content_type: String?) -> void
23
+ end
24
+
25
+ class Tag
26
+ attr_reader name: String
27
+ attr_reader value: String
28
+
29
+ def initialize: (name: String, ?value: String) -> void
30
+ end
31
+
32
+ class Email
33
+ attr_reader id: String
34
+ attr_reader message_id: String?
35
+ attr_reader idempotent_replayed: bool
36
+ attr_reader status: String?
37
+ attr_reader scheduled_at: String?
38
+ attr_reader batch_id: String?
39
+
40
+ def initialize: (id: String, ?message_id: String?, ?idempotent_replayed: bool, ?status: String?,
41
+ ?scheduled_at: String?, ?batch_id: String?) -> void
42
+ def scheduled?: () -> bool
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ module Mailkube
2
+ module Webhooks
3
+ DEFAULT_TOLERANCE: Integer
4
+ SIGNATURE_PREFIX: String
5
+ PARSE_OPTIONS: Hash[Symbol, untyped]
6
+
7
+ # `headers` is any mapping that yields name/value pairs, not just a Hash:
8
+ # `ActionDispatch::Http::Headers` is Enumerable-only and yields CGI env names, and a Rails
9
+ # receiver passes it straight through from `request.headers`.
10
+ def self.verify_signature: (payload: String, headers: _HeaderMap, secret: String,
11
+ ?tolerance: Integer) -> String
12
+ def self.parse_event: (String) -> Events::Event
13
+ def self.verify: (payload: String, headers: _HeaderMap, secret: String,
14
+ ?tolerance: Integer) -> Events::Event
15
+ def self.check_freshness: (String, Integer) -> void
16
+ def self.check_signature: (String, String, String, String, String) -> void
17
+ def self.normalize_headers: (_HeaderMap) -> Hash[String, String]
18
+ def self.canonical_header: (untyped) -> String
19
+ end
20
+
21
+ # The narrowest thing a header mapping has to be: something that yields name/value pairs.
22
+ #
23
+ # The block takes a **pair**, matching how RBS declares `Hash#each`, so a plain Hash satisfies
24
+ # this. Ruby's block destructuring means `{ |name, value| ... }` reads a pair either way, which
25
+ # is also what lets a mapping that yields two arguments — `ActionDispatch::Http::Headers` — work
26
+ # at runtime.
27
+ interface _HeaderMap
28
+ def each: () { ([untyped, untyped]) -> void } -> void
29
+ end
30
+ end
data/sig/mailkube.rbs ADDED
@@ -0,0 +1,36 @@
1
+ # Signatures for the gem's public surface.
2
+ #
3
+ # These are checked two ways, and both are needed:
4
+ # `rbs validate` proves the signatures are internally coherent (every type they name exists).
5
+ # `steep check` proves the implementation matches them.
6
+ # `rbs validate` alone never loads `lib/`, so it would happily pass a `sig/` describing methods
7
+ # that do not exist, which is the exact drift this directory is here to prevent.
8
+ module Mailkube
9
+ VERSION: String
10
+ DEFAULT_BASE_URL: String
11
+
12
+ # What an HTTP adapter must respond to. `Client.new(http:)` accepts anything matching this.
13
+ interface _HttpAdapter
14
+ def call: (method: String, url: String, headers: Hash[String, String], ?body: String?) -> HttpResponse
15
+ end
16
+
17
+ # What a resource needs of its transport: the one verb it calls, and nothing else.
18
+ interface _SendTransport
19
+ def send_email: (RequestSpec) -> Email
20
+ end
21
+
22
+ # What a resource needs when the answer is a model rather than a send acknowledgement.
23
+ #
24
+ # As narrow as `_SendTransport`, and a separate interface rather than a widening of it, so the
25
+ # emails resource never acquires a dependency on a verb it does not call. It performs the request
26
+ # and hands back the decoded object; naming the model is the resource's job — which is also why
27
+ # this needs no generics, of which Ruby has none.
28
+ interface _JsonTransport
29
+ def request_json: (RequestSpec) -> Hash[String, untyped]
30
+ end
31
+
32
+ def self.new: (?api_key: String?, ?base_url: String?, ?timeout: Numeric, ?http: _HttpAdapter?) -> Client
33
+ def self.error_class_for: (Integer) -> Class
34
+
35
+ STATUS_ERRORS: Hash[Integer, Class]
36
+ end