agdirlabs-email 0.1.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/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +284 -0
- data/lib/agdirlabs/email/client.rb +65 -0
- data/lib/agdirlabs/email/delivery_method.rb +143 -0
- data/lib/agdirlabs/email/errors.rb +88 -0
- data/lib/agdirlabs/email/models.rb +172 -0
- data/lib/agdirlabs/email/railtie.rb +17 -0
- data/lib/agdirlabs/email/resources/domains.rb +65 -0
- data/lib/agdirlabs/email/resources/emails.rb +119 -0
- data/lib/agdirlabs/email/resources/webhooks.rb +135 -0
- data/lib/agdirlabs/email/transport.rb +161 -0
- data/lib/agdirlabs/email/version.rb +10 -0
- data/lib/agdirlabs/email/webhook.rb +92 -0
- data/lib/agdirlabs/email/wire.rb +164 -0
- data/lib/agdirlabs/email.rb +30 -0
- data/sig/agdirlabs/email.rbs +318 -0
- metadata +64 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgdirLabs
|
|
4
|
+
module Email
|
|
5
|
+
# Verification of signed webhook requests (Standard Webhooks: `webhook-id`,
|
|
6
|
+
# `webhook-timestamp`, `webhook-signature`).
|
|
7
|
+
module Webhook
|
|
8
|
+
# Alias of {AgdirLabs::Email::WebhookVerificationError}.
|
|
9
|
+
VerificationError = WebhookVerificationError
|
|
10
|
+
|
|
11
|
+
# Default maximum age (and clock skew) of a message, in seconds.
|
|
12
|
+
DEFAULT_TOLERANCE = 300
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Checks that a webhook request came from Agdir Email and returns the parsed event.
|
|
17
|
+
#
|
|
18
|
+
# event = AgdirLabs::Email::Webhook.verify(
|
|
19
|
+
# payload: request.raw_post, headers: request.headers, secret: ENV["AGDIR_EMAIL_WEBHOOK_SECRET"]
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
# @param payload [String] the raw request body, exactly as received (not re-serialized JSON)
|
|
23
|
+
# @param headers [Hash, #[]] request headers: a Hash (any key case, Rack `HTTP_*` keys work too),
|
|
24
|
+
# or any object answering `headers["webhook-id"]` (e.g. Rails `request.headers`)
|
|
25
|
+
# @param secret [String] the webhook's `whsec_…` signing secret
|
|
26
|
+
# @param tolerance [Integer] reject messages older or newer than this many seconds
|
|
27
|
+
# @param now [Time] for tests
|
|
28
|
+
# @return [WebhookEvent]
|
|
29
|
+
# @raise [WebhookVerificationError]
|
|
30
|
+
def verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE, now: Time.now)
|
|
31
|
+
id = header(headers, "webhook-id")
|
|
32
|
+
timestamp = header(headers, "webhook-timestamp")
|
|
33
|
+
signatures = header(headers, "webhook-signature")
|
|
34
|
+
raise WebhookVerificationError, "Missing webhook signature headers" unless id && timestamp && signatures
|
|
35
|
+
|
|
36
|
+
sent_at = Integer(timestamp, 10, exception: false)
|
|
37
|
+
if sent_at.nil? || (now.to_i - sent_at).abs > tolerance
|
|
38
|
+
raise WebhookVerificationError, "Webhook timestamp is outside the allowed tolerance"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
expected = sign(secret: secret, id: id, timestamp: timestamp, payload: payload)
|
|
42
|
+
# The header may hold several space-separated `v1,<sig>` entries (e.g. during secret rotation).
|
|
43
|
+
valid = signatures.split.any? do |entry|
|
|
44
|
+
entry.start_with?("v1,") && secure_compare(entry.delete_prefix("v1,"), expected)
|
|
45
|
+
end
|
|
46
|
+
raise WebhookVerificationError, "Webhook signature does not match" unless valid
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
json = JSON.parse(payload)
|
|
50
|
+
raise WebhookVerificationError, "Webhook payload is not a JSON object" unless json.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
Wire.webhook_event(json)
|
|
53
|
+
rescue JSON::ParserError
|
|
54
|
+
raise WebhookVerificationError, "Webhook payload is not valid JSON"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Computes the base64 `v1` signature of a message. Useful to sign test requests.
|
|
59
|
+
# @return [String]
|
|
60
|
+
# @raise [WebhookVerificationError] when the secret is not valid base64
|
|
61
|
+
def sign(secret:, id:, timestamp:, payload:)
|
|
62
|
+
encoded = secret.to_s.delete_prefix("whsec_")
|
|
63
|
+
key = begin
|
|
64
|
+
encoded.unpack1("m0")
|
|
65
|
+
rescue ArgumentError
|
|
66
|
+
raise WebhookVerificationError, "Signing secret is not valid base64"
|
|
67
|
+
end
|
|
68
|
+
[OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{timestamp}.#{payload}")].pack("m0")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @api private
|
|
72
|
+
def header(headers, name)
|
|
73
|
+
value = if headers.is_a?(Hash)
|
|
74
|
+
rack = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
75
|
+
match = headers.find { |k, _| k.to_s.casecmp?(name) || k.to_s == rack }
|
|
76
|
+
match&.last
|
|
77
|
+
else
|
|
78
|
+
headers[name]
|
|
79
|
+
end
|
|
80
|
+
value = value.first if value.is_a?(Array)
|
|
81
|
+
value.nil? || value.to_s.empty? ? nil : value.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @api private
|
|
85
|
+
def secure_compare(left, right)
|
|
86
|
+
return false unless left.bytesize == right.bytesize
|
|
87
|
+
|
|
88
|
+
OpenSSL.fixed_length_secure_compare(left, right)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgdirLabs
|
|
4
|
+
module Email
|
|
5
|
+
# Mapping between the SDK's Ruby objects and the API's JSON.
|
|
6
|
+
# @api private
|
|
7
|
+
module Wire
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def time(value) = Time.iso8601(value.to_s)
|
|
11
|
+
def optional_time(value) = value.nil? || value == "" ? nil : time(value)
|
|
12
|
+
def list(value) = (value.nil? ? [] : Array(value)).map { |v| str(v) }.freeze
|
|
13
|
+
def str(value) = value.nil? ? nil : value.to_s.dup.freeze
|
|
14
|
+
|
|
15
|
+
def deep_freeze(value)
|
|
16
|
+
case value
|
|
17
|
+
when Hash then value.each_value { |v| deep_freeze(v) }.freeze
|
|
18
|
+
when Array then value.each { |v| deep_freeze(v) }.freeze
|
|
19
|
+
else value.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Builds a `POST /emails` body from keyword arguments (snake_case on both sides).
|
|
24
|
+
def send_email_body(params)
|
|
25
|
+
params = params.transform_keys(&:to_sym)
|
|
26
|
+
unknown = params.keys - SEND_KEYS
|
|
27
|
+
raise ArgumentError, "unknown email field(s): #{unknown.join(", ")}" unless unknown.empty?
|
|
28
|
+
|
|
29
|
+
scheduled_at = params[:scheduled_at]
|
|
30
|
+
scheduled_at = scheduled_at.iso8601 if scheduled_at.respond_to?(:iso8601) && !scheduled_at.is_a?(String)
|
|
31
|
+
tags = params[:tags]
|
|
32
|
+
tags = tags.map { |name, value| { name: name.to_s, value: value.to_s } } if tags.is_a?(Hash)
|
|
33
|
+
{
|
|
34
|
+
from: params[:from],
|
|
35
|
+
to: params[:to],
|
|
36
|
+
subject: params[:subject],
|
|
37
|
+
html: params[:html],
|
|
38
|
+
text: params[:text],
|
|
39
|
+
cc: params[:cc],
|
|
40
|
+
bcc: params[:bcc],
|
|
41
|
+
reply_to: params[:reply_to],
|
|
42
|
+
headers: params[:headers]&.to_h { |k, v| [k.to_s, v.to_s] },
|
|
43
|
+
tags: tags&.map { |t| { name: (t[:name] || t["name"]).to_s, value: (t[:value] || t["value"]).to_s } },
|
|
44
|
+
scheduled_at: scheduled_at
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
SEND_KEYS = %i[from to subject html text cc bcc reply_to headers tags scheduled_at].freeze
|
|
49
|
+
|
|
50
|
+
def email(json)
|
|
51
|
+
EmailDetails.new(
|
|
52
|
+
id: str(json["id"]),
|
|
53
|
+
from: str(json["from"]),
|
|
54
|
+
to: list(json["to"]),
|
|
55
|
+
cc: list(json["cc"]),
|
|
56
|
+
bcc: list(json["bcc"]),
|
|
57
|
+
reply_to: list(json["reply_to"]),
|
|
58
|
+
subject: str(json["subject"]),
|
|
59
|
+
html: str(json["html"]),
|
|
60
|
+
text: str(json["text"]),
|
|
61
|
+
created_at: time(json["created_at"]),
|
|
62
|
+
scheduled_at: optional_time(json["scheduled_at"]),
|
|
63
|
+
status: str(json["last_event"])
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def record(json)
|
|
68
|
+
DnsRecord.new(
|
|
69
|
+
purpose: str(json["record"]),
|
|
70
|
+
name: str(json["name"]),
|
|
71
|
+
type: str(json["type"]),
|
|
72
|
+
value: str(json["value"]),
|
|
73
|
+
ttl: str(json["ttl"]),
|
|
74
|
+
priority: json["priority"],
|
|
75
|
+
status: str(json["status"])
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def domain(json)
|
|
80
|
+
Domain.new(
|
|
81
|
+
id: str(json["id"]),
|
|
82
|
+
name: str(json["name"]),
|
|
83
|
+
status: str(json["status"]),
|
|
84
|
+
region: str(json["region"]),
|
|
85
|
+
created_at: time(json["created_at"]),
|
|
86
|
+
open_tracking: json["open_tracking"] ? true : false,
|
|
87
|
+
click_tracking: json["click_tracking"] ? true : false,
|
|
88
|
+
records: json.key?("records") ? (json["records"] || []).map { |r| record(r) }.freeze : nil
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def webhook(json)
|
|
93
|
+
WebhookEndpoint.new(
|
|
94
|
+
id: str(json["id"]),
|
|
95
|
+
endpoint: str(json["endpoint"]),
|
|
96
|
+
status: str(json["status"]),
|
|
97
|
+
events: list(json["events"]),
|
|
98
|
+
created_at: time(json["created_at"]),
|
|
99
|
+
signing_secret: str(json["signing_secret"])
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delivery(json)
|
|
104
|
+
WebhookDelivery.new(
|
|
105
|
+
id: str(json["id"]),
|
|
106
|
+
type: str(json["type"]),
|
|
107
|
+
status: str(json["status"]),
|
|
108
|
+
created_at: time(json["created_at"]),
|
|
109
|
+
next_attempt_at: optional_time(json["next_attempt_at"]),
|
|
110
|
+
payload: deep_freeze(json["payload"])
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def attempt(json)
|
|
115
|
+
WebhookAttempt.new(
|
|
116
|
+
id: str(json["id"]),
|
|
117
|
+
http_status: json["http_status_code"].to_i,
|
|
118
|
+
response: str(json["response"]),
|
|
119
|
+
sent_at: time(json["sent_at"])
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def page(json, &)
|
|
124
|
+
items = json["data"] || []
|
|
125
|
+
has_more = json["has_more"] ? true : false
|
|
126
|
+
last = items.last
|
|
127
|
+
Page.new(
|
|
128
|
+
data: items.map(&),
|
|
129
|
+
has_more: has_more,
|
|
130
|
+
next_cursor: has_more && last ? last["id"].to_s.freeze : nil
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def webhook_event(json)
|
|
135
|
+
d = json["data"] || {}
|
|
136
|
+
WebhookEvent.new(
|
|
137
|
+
type: str(json["type"]),
|
|
138
|
+
created_at: time(json["created_at"]),
|
|
139
|
+
data: WebhookEventData.new(
|
|
140
|
+
email_id: str(d["email_id"]),
|
|
141
|
+
message_id: str(d["message_id"]),
|
|
142
|
+
from: str(d["from"]),
|
|
143
|
+
to: list(d["to"]),
|
|
144
|
+
subject: str(d["subject"]),
|
|
145
|
+
tags: deep_freeze((d["tags"] || {}).to_h { |k, v| [k.to_s, v.to_s] }),
|
|
146
|
+
created_at: time(d["created_at"]),
|
|
147
|
+
bounce: d["bounce"] && bounce(d["bounce"]),
|
|
148
|
+
click: d["click"] && click(d["click"]),
|
|
149
|
+
failed: d["failed"] && Failure.new(reason: str(d["failed"]["reason"]))
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def bounce(json)
|
|
155
|
+
Bounce.new(type: str(json["type"]), sub_type: str(json["subType"]), message: str(json["message"]))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def click(json)
|
|
159
|
+
Click.new(link: str(json["link"]), ip_address: str(json["ipAddress"]), user_agent: str(json["userAgent"]),
|
|
160
|
+
timestamp: optional_time(json["timestamp"]))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module AgdirLabs
|
|
11
|
+
# The official Ruby SDK for Agdir Email (https://agdir.email).
|
|
12
|
+
#
|
|
13
|
+
# client = AgdirLabs::Email::Client.new(api_key: "re_…")
|
|
14
|
+
# client.emails.send(from: "Acme <hi@acme.com>", to: "ana@example.com", subject: "Hi", html: "<p>Hi</p>")
|
|
15
|
+
module Email
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "email/version"
|
|
20
|
+
require_relative "email/errors"
|
|
21
|
+
require_relative "email/models"
|
|
22
|
+
require_relative "email/wire"
|
|
23
|
+
require_relative "email/transport"
|
|
24
|
+
require_relative "email/resources/emails"
|
|
25
|
+
require_relative "email/resources/domains"
|
|
26
|
+
require_relative "email/resources/webhooks"
|
|
27
|
+
require_relative "email/client"
|
|
28
|
+
require_relative "email/webhook"
|
|
29
|
+
require_relative "email/delivery_method"
|
|
30
|
+
require_relative "email/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
module AgdirLabs
|
|
2
|
+
module Email
|
|
3
|
+
VERSION: String
|
|
4
|
+
USER_AGENT: String
|
|
5
|
+
DEFAULT_BASE_URL: String
|
|
6
|
+
|
|
7
|
+
type address = String
|
|
8
|
+
type addresses = String | Array[String]
|
|
9
|
+
type tags = Array[{ name: String, value: String }] | Hash[String | Symbol, untyped]
|
|
10
|
+
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
CODES: Hash[String, singleton(Error)]
|
|
13
|
+
|
|
14
|
+
attr_reader code: String
|
|
15
|
+
attr_reader status: Integer
|
|
16
|
+
attr_reader retry_after: Float?
|
|
17
|
+
|
|
18
|
+
def initialize: (?String? message, ?code: String, ?status: Integer, ?retry_after: Float?) -> void
|
|
19
|
+
def self.build: (String message, code: String, status: Integer, ?retry_after: Float?) -> Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class AuthenticationError < Error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ValidationError < Error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class NotFoundError < Error
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class IdempotencyError < Error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class RateLimitError < Error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class QuotaExceededError < Error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class ServerError < Error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class ConnectionError < Error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class TimeoutError < ConnectionError
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class ConfigurationError < ArgumentError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class WebhookVerificationError < StandardError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class UnsupportedMessageError < ArgumentError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class SendResult
|
|
59
|
+
attr_reader id: String
|
|
60
|
+
def initialize: (id: String) -> void
|
|
61
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class BatchError
|
|
65
|
+
attr_reader index: Integer
|
|
66
|
+
attr_reader message: String
|
|
67
|
+
def initialize: (index: Integer, message: String) -> void
|
|
68
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class BatchResult
|
|
72
|
+
attr_reader data: Array[SendResult]
|
|
73
|
+
attr_reader errors: Array[BatchError]
|
|
74
|
+
def initialize: (data: Array[SendResult], errors: Array[BatchError]) -> void
|
|
75
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Result
|
|
79
|
+
attr_reader id: String
|
|
80
|
+
attr_reader deleted: bool
|
|
81
|
+
def initialize: (id: String, deleted: bool) -> void
|
|
82
|
+
def deleted?: () -> bool
|
|
83
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class EmailDetails
|
|
87
|
+
attr_reader id: String
|
|
88
|
+
attr_reader from: String
|
|
89
|
+
attr_reader to: Array[String]
|
|
90
|
+
attr_reader cc: Array[String]
|
|
91
|
+
attr_reader bcc: Array[String]
|
|
92
|
+
attr_reader reply_to: Array[String]
|
|
93
|
+
attr_reader subject: String
|
|
94
|
+
attr_reader html: String?
|
|
95
|
+
attr_reader text: String?
|
|
96
|
+
attr_reader created_at: Time
|
|
97
|
+
attr_reader scheduled_at: Time?
|
|
98
|
+
attr_reader status: String
|
|
99
|
+
def initialize: (**untyped) -> void
|
|
100
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class DnsRecord
|
|
104
|
+
attr_reader purpose: String
|
|
105
|
+
attr_reader name: String
|
|
106
|
+
attr_reader type: String
|
|
107
|
+
attr_reader value: String
|
|
108
|
+
attr_reader ttl: String
|
|
109
|
+
attr_reader priority: Integer?
|
|
110
|
+
attr_reader status: String
|
|
111
|
+
def initialize: (**untyped) -> void
|
|
112
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class Domain
|
|
116
|
+
attr_reader id: String
|
|
117
|
+
attr_reader name: String
|
|
118
|
+
attr_reader status: String
|
|
119
|
+
attr_reader region: String
|
|
120
|
+
attr_reader created_at: Time
|
|
121
|
+
attr_reader open_tracking: bool
|
|
122
|
+
attr_reader click_tracking: bool
|
|
123
|
+
attr_reader records: Array[DnsRecord]?
|
|
124
|
+
def initialize: (**untyped) -> void
|
|
125
|
+
def verified?: () -> bool
|
|
126
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class WebhookEndpoint
|
|
130
|
+
attr_reader id: String
|
|
131
|
+
attr_reader endpoint: String
|
|
132
|
+
attr_reader status: String
|
|
133
|
+
attr_reader events: Array[String]
|
|
134
|
+
attr_reader created_at: Time
|
|
135
|
+
attr_reader signing_secret: String?
|
|
136
|
+
def initialize: (**untyped) -> void
|
|
137
|
+
def enabled?: () -> bool
|
|
138
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class WebhookSecret
|
|
142
|
+
attr_reader id: String
|
|
143
|
+
attr_reader signing_secret: String
|
|
144
|
+
def initialize: (id: String, signing_secret: String) -> void
|
|
145
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class WebhookDelivery
|
|
149
|
+
attr_reader id: String
|
|
150
|
+
attr_reader type: String
|
|
151
|
+
attr_reader status: String
|
|
152
|
+
attr_reader created_at: Time
|
|
153
|
+
attr_reader next_attempt_at: Time?
|
|
154
|
+
attr_reader payload: Hash[String, untyped]?
|
|
155
|
+
def initialize: (**untyped) -> void
|
|
156
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class WebhookAttempt
|
|
160
|
+
attr_reader id: String
|
|
161
|
+
attr_reader http_status: Integer
|
|
162
|
+
attr_reader response: String
|
|
163
|
+
attr_reader sent_at: Time
|
|
164
|
+
def initialize: (**untyped) -> void
|
|
165
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class Bounce
|
|
169
|
+
attr_reader type: String
|
|
170
|
+
attr_reader sub_type: String
|
|
171
|
+
attr_reader message: String
|
|
172
|
+
def initialize: (type: String, sub_type: String, message: String) -> void
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class Click
|
|
176
|
+
attr_reader link: String
|
|
177
|
+
attr_reader ip_address: String
|
|
178
|
+
attr_reader user_agent: String
|
|
179
|
+
attr_reader timestamp: Time?
|
|
180
|
+
def initialize: (link: String, ip_address: String, user_agent: String, timestamp: Time?) -> void
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class Failure
|
|
184
|
+
attr_reader reason: String
|
|
185
|
+
def initialize: (reason: String) -> void
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
class WebhookEventData
|
|
189
|
+
attr_reader email_id: String
|
|
190
|
+
attr_reader message_id: String?
|
|
191
|
+
attr_reader from: String
|
|
192
|
+
attr_reader to: Array[String]
|
|
193
|
+
attr_reader subject: String
|
|
194
|
+
attr_reader tags: Hash[String, String]
|
|
195
|
+
attr_reader created_at: Time
|
|
196
|
+
attr_reader bounce: Bounce?
|
|
197
|
+
attr_reader click: Click?
|
|
198
|
+
attr_reader failed: Failure?
|
|
199
|
+
def initialize: (**untyped) -> void
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class WebhookEvent
|
|
203
|
+
attr_reader type: String
|
|
204
|
+
attr_reader created_at: Time
|
|
205
|
+
attr_reader data: WebhookEventData
|
|
206
|
+
def initialize: (type: String, created_at: Time, data: WebhookEventData) -> void
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class Page[T]
|
|
210
|
+
include Enumerable[T]
|
|
211
|
+
|
|
212
|
+
attr_reader data: Array[T]
|
|
213
|
+
attr_reader has_more: bool
|
|
214
|
+
attr_reader next_cursor: String?
|
|
215
|
+
|
|
216
|
+
def initialize: (data: Array[T], has_more: bool, next_cursor: String?) -> void
|
|
217
|
+
def has_more?: () -> bool
|
|
218
|
+
def each: () { (T) -> void } -> Array[T]
|
|
219
|
+
| () -> Enumerator[T, Array[T]]
|
|
220
|
+
def size: () -> Integer
|
|
221
|
+
def length: () -> Integer
|
|
222
|
+
def empty?: () -> bool
|
|
223
|
+
def to_h: () -> { data: Array[T], has_more: bool, next_cursor: String? }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class HttpResponse
|
|
227
|
+
attr_reader status: Integer
|
|
228
|
+
attr_reader headers: Hash[String, String]
|
|
229
|
+
attr_reader body: String
|
|
230
|
+
def initialize: (status: Integer, headers: Hash[String, String], body: String) -> void
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
interface _HttpClient
|
|
234
|
+
def call: (method: String, url: URI::Generic, headers: Hash[String, String], body: String?, timeout: Numeric) -> HttpResponse
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class NetHttpClient
|
|
238
|
+
include _HttpClient
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class Client
|
|
242
|
+
attr_reader emails: Resources::Emails
|
|
243
|
+
attr_reader domains: Resources::Domains
|
|
244
|
+
attr_reader webhooks: Resources::Webhooks
|
|
245
|
+
attr_reader base_url: String
|
|
246
|
+
attr_reader max_retries: Integer
|
|
247
|
+
attr_reader timeout: Numeric
|
|
248
|
+
|
|
249
|
+
def initialize: (?api_key: String?, ?base_url: String?, ?max_retries: Integer, ?timeout: Numeric, ?http_client: _HttpClient?) -> void
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
module Resources
|
|
253
|
+
class Emails
|
|
254
|
+
MAX_BATCH_SIZE: Integer
|
|
255
|
+
|
|
256
|
+
def send: (from: address, to: addresses, subject: String, ?html: String?, ?text: String?, ?cc: addresses?,
|
|
257
|
+
?bcc: addresses?, ?reply_to: addresses?, ?headers: Hash[String | Symbol, untyped]?, ?tags: tags?,
|
|
258
|
+
?scheduled_at: Time | String | nil, ?idempotency_key: String?) -> SendResult
|
|
259
|
+
def send_batch: (Array[Hash[Symbol, untyped]] emails, ?validation: :strict | :permissive | String, ?idempotency_key: String?) -> BatchResult
|
|
260
|
+
def get: (String id) -> EmailDetails
|
|
261
|
+
def cancel: (String id) -> Result
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
class Domains
|
|
265
|
+
def create: (name: String, ?region: String?) -> Domain
|
|
266
|
+
def get: (String id) -> Domain
|
|
267
|
+
def list: (?limit: Integer?, ?after: String?) -> Page[Domain]
|
|
268
|
+
def list_all: (?limit: Integer?, ?after: String?) -> Enumerator[Domain, void]
|
|
269
|
+
def verify: (String id) -> Result
|
|
270
|
+
def delete: (String id) -> Result
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
class WebhookDeliveries
|
|
274
|
+
def list: (String webhook_id, ?limit: Integer?, ?after: String?) -> Page[WebhookDelivery]
|
|
275
|
+
def list_all: (String webhook_id, ?limit: Integer?, ?after: String?) -> Enumerator[WebhookDelivery, void]
|
|
276
|
+
def get: (String webhook_id, String delivery_id) -> WebhookDelivery
|
|
277
|
+
def attempts: (String webhook_id, String delivery_id, ?limit: Integer?, ?after: String?) -> Page[WebhookAttempt]
|
|
278
|
+
def replay: (String webhook_id, String delivery_id) -> Result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
class Webhooks
|
|
282
|
+
EVENT_TYPES: Array[String]
|
|
283
|
+
|
|
284
|
+
attr_reader deliveries: WebhookDeliveries
|
|
285
|
+
|
|
286
|
+
def create: (endpoint: String, events: Array[String] | String) -> WebhookSecret
|
|
287
|
+
def get: (String id) -> WebhookEndpoint
|
|
288
|
+
def list: (?limit: Integer?, ?after: String?) -> Page[WebhookEndpoint]
|
|
289
|
+
def list_all: (?limit: Integer?, ?after: String?) -> Enumerator[WebhookEndpoint, void]
|
|
290
|
+
def update: (String id, ?endpoint: String?, ?events: Array[String] | String | nil, ?status: String | Symbol | nil) -> Result
|
|
291
|
+
def delete: (String id) -> Result
|
|
292
|
+
def rotate_secret: (String id) -> WebhookSecret
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
module Webhook
|
|
297
|
+
VerificationError: singleton(WebhookVerificationError)
|
|
298
|
+
DEFAULT_TOLERANCE: Integer
|
|
299
|
+
|
|
300
|
+
def self.verify: (payload: String, headers: untyped, secret: String, ?tolerance: Integer, ?now: Time) -> WebhookEvent
|
|
301
|
+
def self.sign: (secret: String, id: String, timestamp: String | Integer, payload: String) -> String
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
class DeliveryMethod
|
|
305
|
+
TAGS_HEADER: String
|
|
306
|
+
SCHEDULED_AT_HEADER: String
|
|
307
|
+
IDEMPOTENCY_KEY_HEADER: String
|
|
308
|
+
EMAIL_ID_HEADER: String
|
|
309
|
+
|
|
310
|
+
attr_accessor settings: Hash[Symbol, untyped]
|
|
311
|
+
|
|
312
|
+
def initialize: (?Hash[String | Symbol, untyped]? settings) -> void
|
|
313
|
+
def deliver!: (untyped mail) -> SendResult
|
|
314
|
+
def client: () -> Client
|
|
315
|
+
def self.to_params: (untyped mail) -> [Hash[Symbol, untyped], String?]
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agdirlabs-email
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Agdir AS
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Send transactional email, manage sending domains and webhooks, and verify
|
|
13
|
+
signed webhook events with the Agdir Email API. Includes an ActionMailer / mail
|
|
14
|
+
gem delivery method. No runtime dependencies.
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/agdirlabs/email.rb
|
|
23
|
+
- lib/agdirlabs/email/client.rb
|
|
24
|
+
- lib/agdirlabs/email/delivery_method.rb
|
|
25
|
+
- lib/agdirlabs/email/errors.rb
|
|
26
|
+
- lib/agdirlabs/email/models.rb
|
|
27
|
+
- lib/agdirlabs/email/railtie.rb
|
|
28
|
+
- lib/agdirlabs/email/resources/domains.rb
|
|
29
|
+
- lib/agdirlabs/email/resources/emails.rb
|
|
30
|
+
- lib/agdirlabs/email/resources/webhooks.rb
|
|
31
|
+
- lib/agdirlabs/email/transport.rb
|
|
32
|
+
- lib/agdirlabs/email/version.rb
|
|
33
|
+
- lib/agdirlabs/email/webhook.rb
|
|
34
|
+
- lib/agdirlabs/email/wire.rb
|
|
35
|
+
- sig/agdirlabs/email.rbs
|
|
36
|
+
homepage: https://agdir.email
|
|
37
|
+
licenses:
|
|
38
|
+
- MIT
|
|
39
|
+
metadata:
|
|
40
|
+
homepage_uri: https://agdir.email
|
|
41
|
+
source_code_uri: https://github.com/AgdirAS/agdirlabs-sdks/tree/main/ruby
|
|
42
|
+
changelog_uri: https://github.com/AgdirAS/agdirlabs-sdks/blob/main/ruby/CHANGELOG.md
|
|
43
|
+
bug_tracker_uri: https://github.com/AgdirAS/agdirlabs-sdks/issues
|
|
44
|
+
documentation_uri: https://github.com/AgdirAS/agdirlabs-sdks/tree/main/ruby#readme
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.6.9
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Official Ruby SDK for Agdir Email — programmatic SMTP for developers, hosted
|
|
63
|
+
in the EU.
|
|
64
|
+
test_files: []
|