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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +11 -0
- data/README.md +323 -0
- data/lib/mailkube/client.rb +52 -0
- data/lib/mailkube/config.rb +89 -0
- data/lib/mailkube/errors.rb +132 -0
- data/lib/mailkube/events/contexts.rb +129 -0
- data/lib/mailkube/events/envelopes.rb +96 -0
- data/lib/mailkube/events/node.rb +62 -0
- data/lib/mailkube/events/payloads.rb +88 -0
- data/lib/mailkube/events/registry.rb +26 -0
- data/lib/mailkube/logging.rb +113 -0
- data/lib/mailkube/net_http_adapter.rb +123 -0
- data/lib/mailkube/resources/emails.rb +81 -0
- data/lib/mailkube/resources/scheduled_email_requests.rb +78 -0
- data/lib/mailkube/resources/scheduled_emails.rb +142 -0
- data/lib/mailkube/serialization.rb +98 -0
- data/lib/mailkube/transport.rb +190 -0
- data/lib/mailkube/types/scheduled_acks.rb +57 -0
- data/lib/mailkube/types/scheduled_emails.rb +96 -0
- data/lib/mailkube/types.rb +54 -0
- data/lib/mailkube/version.rb +15 -0
- data/lib/mailkube/webhooks.rb +152 -0
- data/lib/mailkube.rb +68 -0
- data/sig/mailkube/client.rbs +9 -0
- data/sig/mailkube/config.rbs +14 -0
- data/sig/mailkube/errors.rbs +78 -0
- data/sig/mailkube/events/contexts.rbs +60 -0
- data/sig/mailkube/events/envelopes.rbs +60 -0
- data/sig/mailkube/events/node.rbs +32 -0
- data/sig/mailkube/events/payloads.rbs +54 -0
- data/sig/mailkube/events/registry.rbs +6 -0
- data/sig/mailkube/logging.rbs +26 -0
- data/sig/mailkube/net_http_adapter.rbs +15 -0
- data/sig/mailkube/resources/emails.rbs +18 -0
- data/sig/mailkube/resources/scheduled_email_requests.rbs +19 -0
- data/sig/mailkube/resources/scheduled_emails.rbs +32 -0
- data/sig/mailkube/serialization.rbs +11 -0
- data/sig/mailkube/transport.rbs +34 -0
- data/sig/mailkube/types/scheduled_acks.rbs +30 -0
- data/sig/mailkube/types/scheduled_emails.rbs +53 -0
- data/sig/mailkube/types.rbs +44 -0
- data/sig/mailkube/webhooks.rbs +30 -0
- data/sig/mailkube.rbs +36 -0
- 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,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
|