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.
@@ -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: []