quolle 1.0.0 → 1.2.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 +4 -4
- data/README.md +31 -2
- data/lib/quolle/client.rb +42 -5
- data/lib/quolle/error.rb +6 -1
- data/lib/quolle/resources/api_keys.rb +38 -0
- data/lib/quolle/resources/domains.rb +45 -0
- data/lib/quolle/resources/emails.rb +17 -4
- data/lib/quolle/resources/path.rb +14 -0
- data/lib/quolle/resources/webhooks.rb +67 -2
- data/lib/quolle/version.rb +1 -1
- metadata +7 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49a558cd0e8d55d4a043225faf03055ab2d71b645b0d1fb9480acb5931addc0
|
|
4
|
+
data.tar.gz: 57eaff3fe141479fe6f99e5a84dc226957efb2dc3145f65f0666e9e9b5c8d556
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa4abf51a8b949560722c0fb9e7f4ad55653a6a16fac85f56c7a427ab0fe34ce0a52bad869c48274c76b959287734f48aef984a7134727854e931b9e8f4c1f5f
|
|
7
|
+
data.tar.gz: f04d7e24d159145ef67a7dbcc5c4675637d562dcd62f7eb509428837b1715e5ec735d3f5673d01bed8a531e80fa26310c07895488e75d37c328591da5570d457
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Or in your Gemfile:
|
|
|
14
14
|
gem "quolle"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Ruby
|
|
17
|
+
Requires Ruby 3.3+. No third-party dependencies.
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
@@ -33,6 +33,11 @@ result = quolle.emails.send(
|
|
|
33
33
|
puts "Queued: #{result['id']}"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`emails.create(...)` does exactly the same thing and doesn't shadow Ruby's
|
|
37
|
+
`Object#send`, so prefer it if you use `send` for dynamic dispatch. Pass
|
|
38
|
+
`type: "marketing"` for marketing mail (adds unsubscribe headers); the default
|
|
39
|
+
is transactional.
|
|
40
|
+
|
|
36
41
|
## Sending
|
|
37
42
|
|
|
38
43
|
### Multiple recipients
|
|
@@ -135,6 +140,7 @@ begin
|
|
|
135
140
|
)
|
|
136
141
|
rescue Quolle::Error => e
|
|
137
142
|
puts e.status_code # e.g. 402
|
|
143
|
+
puts e.code # e.g. "monthly_quota_exceeded" — branch on this
|
|
138
144
|
puts e.message # e.g. "Monthly limit reached"
|
|
139
145
|
puts e.data # extra fields, e.g. {"limit" => 3000}
|
|
140
146
|
end
|
|
@@ -165,9 +171,32 @@ end
|
|
|
165
171
|
|
|
166
172
|
HMAC-SHA256 with a 5-minute timestamp window (replay protection).
|
|
167
173
|
|
|
174
|
+
## Domains, API keys and webhooks
|
|
175
|
+
|
|
176
|
+
These need a **full-access** API key. A sending-only key gets `403` with code `restricted_api_key`, so a key you ship in an app can't create keys or change your domains.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Domains
|
|
180
|
+
created = quolle.domains.create("mail.example.com") # created["dnsRecords"] to publish
|
|
181
|
+
quolle.domains.verify(created["domain"]["id"]) # check DNS now
|
|
182
|
+
quolle.domains.list.each { |d| puts "#{d["domain"]} #{d["status"]}" }
|
|
183
|
+
|
|
184
|
+
# API keys
|
|
185
|
+
new_key = quolle.api_keys.create(name: "web app", scope: "sending", domains: ["mail.example.com"])
|
|
186
|
+
puts new_key["key"] # shown once
|
|
187
|
+
quolle.api_keys.remove(new_key["id"])
|
|
188
|
+
|
|
189
|
+
# Webhook endpoints
|
|
190
|
+
hook = quolle.webhooks.create(url: "https://example.com/hooks/quolle", events: %w[email.delivered email.bounced])
|
|
191
|
+
# hook["secret"] (whsec_…) is shown once; store it for quolle.webhooks.verify(...)
|
|
192
|
+
quolle.webhooks.test(hook["id"])
|
|
193
|
+
quolle.webhooks.deliveries(hook["id"], status: "failed").each { |d| quolle.webhooks.redeliver(hook["id"], d["id"]) }
|
|
194
|
+
new_secret = quolle.webhooks.rotate_secret(hook["id"])["secret"]
|
|
195
|
+
```
|
|
196
|
+
|
|
168
197
|
## Automatic retries
|
|
169
198
|
|
|
170
|
-
Transient failures — HTTP 429 (rate limit) and 5xx, plus network errors — are retried automatically with exponential backoff, honoring the `Retry-After` header. To avoid double-sending, a POST is only retried on a 5xx/network error when you pass an idempotency key; a 429 is always safe to retry (the request was never processed). Tune with `max_retries:` on `Quolle.new` (default 3).
|
|
199
|
+
Transient failures — HTTP 429 (rate limit) and 5xx, plus network errors — are retried automatically with exponential backoff, honoring the `Retry-After` header. To avoid double-sending, a POST is only retried on a 5xx/network error when you pass an idempotency key; a 429 is always safe to retry (the request was never processed). Quota limits are the exception: `daily_quota_exceeded` and `warmup_limit_exceeded` (and any `Retry-After` over 60 seconds) are raised immediately, because they reset at a fixed time, not in seconds; check `e.code` and queue the send for later. Tune with `max_retries:` on `Quolle.new` (default 3).
|
|
171
200
|
|
|
172
201
|
## License
|
|
173
202
|
|
data/lib/quolle/client.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "version"
|
|
|
8
8
|
require_relative "error"
|
|
9
9
|
require_relative "resources/emails"
|
|
10
10
|
require_relative "resources/webhooks"
|
|
11
|
+
require_relative "resources/domains"
|
|
12
|
+
require_relative "resources/api_keys"
|
|
11
13
|
|
|
12
14
|
module Quolle
|
|
13
15
|
# The Quolle API client.
|
|
@@ -27,6 +29,18 @@ module Quolle
|
|
|
27
29
|
attr_reader :emails
|
|
28
30
|
# @return [Quolle::Resources::Webhooks]
|
|
29
31
|
attr_reader :webhooks
|
|
32
|
+
# @return [Quolle::Resources::Domains] sending domains (full-access key)
|
|
33
|
+
attr_reader :domains
|
|
34
|
+
# @return [Quolle::Resources::ApiKeys] API keys (full-access key)
|
|
35
|
+
attr_reader :api_keys
|
|
36
|
+
|
|
37
|
+
# 429s that retrying can't fix: the limit resets at a fixed time (midnight
|
|
38
|
+
# UTC, the next billing period), not in seconds. Raised at once so the
|
|
39
|
+
# caller can queue the send instead of blocking in a retry loop.
|
|
40
|
+
NO_RETRY_CODES = %w[daily_quota_exceeded warmup_limit_exceeded monthly_quota_exceeded].freeze
|
|
41
|
+
# A Retry-After longer than this (seconds) means "not soon": raise instead
|
|
42
|
+
# of sleeping through it.
|
|
43
|
+
MAX_RETRY_WAIT = 60
|
|
30
44
|
|
|
31
45
|
# @param api_key [String, nil] your API key (falls back to QUOLLE_API_KEY)
|
|
32
46
|
# @param base_url [String]
|
|
@@ -41,6 +55,8 @@ module Quolle
|
|
|
41
55
|
@max_retries = max_retries
|
|
42
56
|
@emails = Resources::Emails.new(self)
|
|
43
57
|
@webhooks = Resources::Webhooks.new(self)
|
|
58
|
+
@domains = Resources::Domains.new(self)
|
|
59
|
+
@api_keys = Resources::ApiKeys.new(self)
|
|
44
60
|
end
|
|
45
61
|
|
|
46
62
|
# Perform an authenticated request and return the parsed JSON body.
|
|
@@ -62,6 +78,7 @@ module Quolle
|
|
|
62
78
|
klass = {
|
|
63
79
|
"GET" => Net::HTTP::Get,
|
|
64
80
|
"POST" => Net::HTTP::Post,
|
|
81
|
+
"PATCH" => Net::HTTP::Patch,
|
|
65
82
|
"DELETE" => Net::HTTP::Delete
|
|
66
83
|
}.fetch(method.upcase)
|
|
67
84
|
|
|
@@ -89,18 +106,38 @@ module Quolle
|
|
|
89
106
|
raise last_error
|
|
90
107
|
end
|
|
91
108
|
|
|
92
|
-
parsed = resp.body.to_s.empty? ? {} : (JSON.parse(resp.body) rescue {})
|
|
93
109
|
status = resp.code.to_i
|
|
110
|
+
json_ok = true
|
|
111
|
+
parsed =
|
|
112
|
+
if resp.body.to_s.empty?
|
|
113
|
+
{}
|
|
114
|
+
else
|
|
115
|
+
begin
|
|
116
|
+
JSON.parse(resp.body)
|
|
117
|
+
rescue JSON::ParserError
|
|
118
|
+
json_ok = false
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
94
122
|
|
|
95
123
|
if (200..299).cover?(status)
|
|
124
|
+
# A 2xx that isn't JSON (a proxy page, a truncated body) is an error,
|
|
125
|
+
# not an empty success.
|
|
126
|
+
unless json_ok
|
|
127
|
+
raise Quolle::Error.new("Unexpected non-JSON response from the Quolle API",
|
|
128
|
+
status_code: status, data: { "body" => resp.body.to_s[0, 500] })
|
|
129
|
+
end
|
|
96
130
|
return parsed
|
|
97
131
|
end
|
|
98
132
|
|
|
99
133
|
message = parsed.delete("error") || "Request failed with status #{status}"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
code = parsed.delete("code")
|
|
135
|
+
last_error = Quolle::Error.new(message, status_code: status, data: parsed, code: code)
|
|
136
|
+
retryable = (status == 429 && !NO_RETRY_CODES.include?(code)) ||
|
|
137
|
+
((500..504).cover?(status) && (idempotent || has_idem))
|
|
138
|
+
wait = backoff(attempt, resp["Retry-After"])
|
|
139
|
+
if attempt < @max_retries && retryable && wait <= MAX_RETRY_WAIT
|
|
140
|
+
sleep(wait)
|
|
104
141
|
next
|
|
105
142
|
end
|
|
106
143
|
raise last_error
|
data/lib/quolle/error.rb
CHANGED
|
@@ -8,11 +8,16 @@ module Quolle
|
|
|
8
8
|
attr_reader :status_code
|
|
9
9
|
# @return [Hash] extra fields returned alongside +error+.
|
|
10
10
|
attr_reader :data
|
|
11
|
+
# @return [String, nil] the API's stable, machine-readable error code
|
|
12
|
+
# (e.g. "validation_error", "rate_limit_exceeded"). Branch on this, not
|
|
13
|
+
# on the message. nil for transport failures.
|
|
14
|
+
attr_reader :code
|
|
11
15
|
|
|
12
|
-
def initialize(message, status_code: nil, data: {})
|
|
16
|
+
def initialize(message, status_code: nil, data: {}, code: nil)
|
|
13
17
|
super(message)
|
|
14
18
|
@status_code = status_code
|
|
15
19
|
@data = data || {}
|
|
20
|
+
@code = code
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path"
|
|
4
|
+
|
|
5
|
+
module Quolle
|
|
6
|
+
module Resources
|
|
7
|
+
# API keys (+client.api_keys+). Needs a full-access key; a sending-only key
|
|
8
|
+
# gets 403 +restricted_api_key+.
|
|
9
|
+
class ApiKeys
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Array<Hash>] active keys (only a +preview+ of each key)
|
|
15
|
+
def list
|
|
16
|
+
@client.request("GET", "/v1/keys")["keys"] || []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Create a key. The returned +key+ (qle_…) is shown once; store it now.
|
|
20
|
+
#
|
|
21
|
+
# @param scope ["full", "sending", nil] "sending" keys can only send email and read its status
|
|
22
|
+
# @param domains [Array<String>, nil] limit the key to these sending domains
|
|
23
|
+
# @param expires_in_days [Integer, nil] days until the key stops working
|
|
24
|
+
def create(name:, scope: nil, domains: nil, expires_in_days: nil)
|
|
25
|
+
body = { name: name }
|
|
26
|
+
body[:scope] = scope if scope
|
|
27
|
+
body[:domains] = domains if domains
|
|
28
|
+
body[:expiresInDays] = expires_in_days if expires_in_days
|
|
29
|
+
@client.request("POST", "/v1/keys", body: body)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Revoke a key. Requests made with it fail from now on.
|
|
33
|
+
def remove(id)
|
|
34
|
+
@client.request("DELETE", "/v1/keys/#{Path.escape(id)}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path"
|
|
4
|
+
|
|
5
|
+
module Quolle
|
|
6
|
+
module Resources
|
|
7
|
+
# Sending domains (+client.domains+). Needs a full-access API key.
|
|
8
|
+
class Domains
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [Array<Hash>] every sending domain, with verification and warm-up state
|
|
14
|
+
def list
|
|
15
|
+
@client.request("GET", "/v1/domains")["domains"] || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Add a domain (e.g. "mail.example.com").
|
|
19
|
+
# @return [Hash] +domain+ and the +dnsRecords+ to publish
|
|
20
|
+
def create(domain)
|
|
21
|
+
@client.request("POST", "/v1/domains", body: { domain: domain })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Hash] one domain
|
|
25
|
+
def get(id)
|
|
26
|
+
@client.request("GET", "/v1/domains/#{Path.escape(id)}")["domain"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Hash] the DNS records to publish, and which are found
|
|
30
|
+
def records(id)
|
|
31
|
+
@client.request("GET", "/v1/domains/#{Path.escape(id)}/records")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check DNS again now instead of waiting for the next automatic check.
|
|
35
|
+
def verify(id)
|
|
36
|
+
@client.request("POST", "/v1/domains/#{Path.escape(id)}/recheck")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Remove a domain. Mail can no longer be sent from it.
|
|
40
|
+
def remove(id)
|
|
41
|
+
@client.request("DELETE", "/v1/domains/#{Path.escape(id)}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -12,15 +12,26 @@ module Quolle
|
|
|
12
12
|
#
|
|
13
13
|
# Provide either +:html+ (with +:subject+) or a +:template+ slug (with
|
|
14
14
|
# optional +:variables+). Pass +idempotency_key:+ to make retries safe.
|
|
15
|
+
# +type:+ (or +message_type:+) is "transactional" (default) or
|
|
16
|
+
# "marketing"; marketing mail gets unsubscribe headers and honours
|
|
17
|
+
# unsubscribes.
|
|
15
18
|
#
|
|
16
19
|
# @param params [Hash] from:, to:, subject:, html:, text:, template:,
|
|
17
|
-
# variables:, reply_to:/replyTo:, scheduled_at:/scheduledAt:, metadata
|
|
20
|
+
# variables:, reply_to:/replyTo:, scheduled_at:/scheduledAt:, metadata:,
|
|
21
|
+
# attachments:, type:
|
|
18
22
|
# @return [Hash] at least "id" and "message"
|
|
19
|
-
def
|
|
23
|
+
def create(idempotency_key: nil, **params)
|
|
20
24
|
headers = idempotency_key ? { "Idempotency-Key" => idempotency_key } : {}
|
|
21
25
|
@client.request("POST", "/v1/emails/send", body: normalize(params), headers: headers)
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# Same as {#create}. Kept because it's the documented name; note that it
|
|
29
|
+
# shadows Object#send on this object, so prefer +create+ in new code
|
|
30
|
+
# (and +public_send+ / +__send__+ for dynamic dispatch).
|
|
31
|
+
def send(idempotency_key: nil, **params)
|
|
32
|
+
create(idempotency_key: idempotency_key, **params)
|
|
33
|
+
end
|
|
34
|
+
|
|
24
35
|
# Send up to 100 emails in one all-or-nothing request.
|
|
25
36
|
#
|
|
26
37
|
# @param emails [Array<Hash>]
|
|
@@ -54,14 +65,16 @@ module Quolle
|
|
|
54
65
|
k = key.to_s
|
|
55
66
|
k = "replyTo" if k == "reply_to"
|
|
56
67
|
k = "scheduledAt" if k == "scheduled_at"
|
|
68
|
+
k = "type" if k == "message_type"
|
|
57
69
|
out[k] = value
|
|
58
70
|
end
|
|
59
71
|
out
|
|
60
72
|
end
|
|
61
73
|
|
|
74
|
+
# Path-segment encoding. (encode_www_form_component turned a space into
|
|
75
|
+
# "+", which is a literal plus in a URL path.)
|
|
62
76
|
def escape(id)
|
|
63
|
-
|
|
64
|
-
URI.encode_www_form_component(id.to_s)
|
|
77
|
+
id.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |ch| format("%%%02X", ch.ord) }
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quolle
|
|
4
|
+
module Resources
|
|
5
|
+
# Path-segment encoding shared by the resources.
|
|
6
|
+
module Path
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def escape(id)
|
|
10
|
+
id.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |ch| format("%%%02X", ch.ord) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -2,14 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "path"
|
|
5
7
|
|
|
6
8
|
module Quolle
|
|
7
9
|
module Resources
|
|
8
|
-
# Verify
|
|
10
|
+
# Verify incoming webhooks, and manage webhook endpoints (+client.webhooks+).
|
|
11
|
+
# +verify+ makes no API call; the management methods need a full-access key.
|
|
9
12
|
class Webhooks
|
|
10
13
|
DEFAULT_TOLERANCE = 300 # seconds
|
|
11
14
|
|
|
12
|
-
def initialize(
|
|
15
|
+
def initialize(client = nil)
|
|
16
|
+
@client = client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<Hash>] the events a webhook can subscribe to
|
|
20
|
+
def events
|
|
21
|
+
req("GET", "/v1/webhooks/events")["events"] || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Array<Hash>]
|
|
25
|
+
def list
|
|
26
|
+
req("GET", "/v1/webhooks")["webhooks"] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add an endpoint. The returned +secret+ (whsec_…) is shown once; store it.
|
|
30
|
+
def create(url:, events:)
|
|
31
|
+
req("POST", "/v1/webhooks", { url: url, events: events })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get(id)
|
|
35
|
+
req("GET", "/v1/webhooks/#{Path.escape(id)}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Change the URL or events. The signing secret stays the same.
|
|
39
|
+
def update(id, url: nil, events: nil)
|
|
40
|
+
body = {}
|
|
41
|
+
body[:url] = url if url
|
|
42
|
+
body[:events] = events if events
|
|
43
|
+
req("PATCH", "/v1/webhooks/#{Path.escape(id)}", body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove(id)
|
|
47
|
+
req("DELETE", "/v1/webhooks/#{Path.escape(id)}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Send a signed +webhook.test+ event now and report how the endpoint answered.
|
|
51
|
+
def test(id)
|
|
52
|
+
req("POST", "/v1/webhooks/#{Path.escape(id)}/test")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Deliveries from the last 30 days, newest first.
|
|
56
|
+
# @param status ["succeeded", "pending", "failed", nil]
|
|
57
|
+
def deliveries(id, status: nil, limit: nil)
|
|
58
|
+
params = { status: status, limit: limit }.compact
|
|
59
|
+
qs = params.empty? ? "" : "?#{URI.encode_www_form(params)}"
|
|
60
|
+
req("GET", "/v1/webhooks/#{Path.escape(id)}/deliveries#{qs}")["deliveries"] || []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Send a delivery again, with the same body and +eventId+.
|
|
64
|
+
def redeliver(id, delivery_id)
|
|
65
|
+
req("POST", "/v1/webhooks/#{Path.escape(id)}/deliveries/#{Path.escape(delivery_id)}/redeliver")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Replace the signing secret. Takes effect immediately; the new +secret+ is returned once.
|
|
69
|
+
def rotate_secret(id)
|
|
70
|
+
req("POST", "/v1/webhooks/#{Path.escape(id)}/rotate-secret")
|
|
71
|
+
end
|
|
13
72
|
|
|
14
73
|
# Verify a webhook signature and return the parsed event.
|
|
15
74
|
#
|
|
@@ -52,6 +111,12 @@ module Quolle
|
|
|
52
111
|
b.each_byte { |byte| res |= byte ^ l.shift }
|
|
53
112
|
res.zero?
|
|
54
113
|
end
|
|
114
|
+
|
|
115
|
+
def req(method, path, body = nil)
|
|
116
|
+
raise Quolle::Error.new("Webhooks was created without a client; use client.webhooks") unless @client
|
|
117
|
+
|
|
118
|
+
body ? @client.request(method, path, body: body) : @client.request(method, path)
|
|
119
|
+
end
|
|
55
120
|
end
|
|
56
121
|
end
|
|
57
122
|
end
|
data/lib/quolle/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quolle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quolle
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: minitest
|
|
@@ -39,7 +38,6 @@ dependencies:
|
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '13.0'
|
|
41
40
|
description: Send transactional email through Quolle from Ruby.
|
|
42
|
-
email:
|
|
43
41
|
executables: []
|
|
44
42
|
extensions: []
|
|
45
43
|
extra_rdoc_files: []
|
|
@@ -49,7 +47,10 @@ files:
|
|
|
49
47
|
- lib/quolle.rb
|
|
50
48
|
- lib/quolle/client.rb
|
|
51
49
|
- lib/quolle/error.rb
|
|
50
|
+
- lib/quolle/resources/api_keys.rb
|
|
51
|
+
- lib/quolle/resources/domains.rb
|
|
52
52
|
- lib/quolle/resources/emails.rb
|
|
53
|
+
- lib/quolle/resources/path.rb
|
|
53
54
|
- lib/quolle/resources/webhooks.rb
|
|
54
55
|
- lib/quolle/version.rb
|
|
55
56
|
homepage: https://quolle.com
|
|
@@ -59,7 +60,6 @@ metadata:
|
|
|
59
60
|
homepage_uri: https://quolle.com
|
|
60
61
|
documentation_uri: https://docs.quolle.com
|
|
61
62
|
source_code_uri: https://github.com/Quolle-main/quolle-ruby
|
|
62
|
-
post_install_message:
|
|
63
63
|
rdoc_options: []
|
|
64
64
|
require_paths:
|
|
65
65
|
- lib
|
|
@@ -67,15 +67,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
67
67
|
requirements:
|
|
68
68
|
- - ">="
|
|
69
69
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: '
|
|
70
|
+
version: '3.3'
|
|
71
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
78
|
-
signing_key:
|
|
77
|
+
rubygems_version: 4.0.20
|
|
79
78
|
specification_version: 4
|
|
80
79
|
summary: Official Ruby SDK for the Quolle email API
|
|
81
80
|
test_files: []
|