mailkite 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/lib/mailkite.rb +167 -0
- metadata +44 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5c90dce58b0e00df106212cea51814eac48aa3bb8d882ccf40d472d36b383597
|
|
4
|
+
data.tar.gz: 1d0c0d768de9751bdbbfe1e90d03053c9b06b9f64a05f93c91f5982f3ade6a1b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 813a0066f5324b66fbb921c275ce35d59749d65a5c04bd69889e2dbb30f3c5a23906400f3b55143c795838d47ef7f305ac94e430f2313cd64be6225e92b15c15
|
|
7
|
+
data.tar.gz: 42ced2df301a792ced60398f3b75d476598dec347ddf180bb0e0c8a66a9863b295910d5ef702b67015d15036f6bb629cff7bb4ef65f4cce1ace9e7a4afbf7cd4
|
data/lib/mailkite.rb
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# MailKite SDK for Ruby.
|
|
2
|
+
#
|
|
3
|
+
# Shape shared by every MailKite SDK: one low-level `request` plus one thin
|
|
4
|
+
# method per API endpoint. Zero dependencies — uses the standard library.
|
|
5
|
+
#
|
|
6
|
+
# require "mailkite"
|
|
7
|
+
# mk = Mailkite::Client.new(ENV["MAILKITE_API_KEY"])
|
|
8
|
+
# res = mk.send({ "from" => ..., "to" => ..., "subject" => ..., "text" => ... })
|
|
9
|
+
|
|
10
|
+
require "net/http"
|
|
11
|
+
require "uri"
|
|
12
|
+
require "json"
|
|
13
|
+
require "openssl"
|
|
14
|
+
|
|
15
|
+
module Mailkite
|
|
16
|
+
VERSION = "0.1.0"
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.mailkite.dev"
|
|
18
|
+
# Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
|
|
19
|
+
DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
|
|
20
|
+
|
|
21
|
+
# Verify an `x-mailkite-signature` header on an inbound webhook delivery.
|
|
22
|
+
# Local HMAC-SHA256 check — no network call. Pass the raw, unparsed body.
|
|
23
|
+
def self.verify_webhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS)
|
|
24
|
+
return false unless signature.is_a?(String) && !signature.empty?
|
|
25
|
+
|
|
26
|
+
parts = {}
|
|
27
|
+
signature.split(",").each do |seg|
|
|
28
|
+
i = seg.index("=")
|
|
29
|
+
next unless i
|
|
30
|
+
parts[seg[0...i].strip] = seg[(i + 1)..].strip
|
|
31
|
+
end
|
|
32
|
+
t = parts["t"]
|
|
33
|
+
v1 = parts["v1"]
|
|
34
|
+
return false if t.nil? || v1.nil? || t.empty? || v1.empty? || !t.match?(/\A-?\d+\z/)
|
|
35
|
+
|
|
36
|
+
# The t in the header is milliseconds since the epoch.
|
|
37
|
+
if tolerance_ms && tolerance_ms > 0
|
|
38
|
+
return false if ((Time.now.to_f * 1000) - t.to_i).abs > tolerance_ms
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{payload}")
|
|
42
|
+
secure_compare(expected, v1)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Length-independent constant-time string compare (no openssl >= 2.2 needed).
|
|
48
|
+
def self.secure_compare(a, b)
|
|
49
|
+
return false unless a.bytesize == b.bytesize
|
|
50
|
+
|
|
51
|
+
diff = 0
|
|
52
|
+
a.bytes.each_with_index { |byte, i| diff |= byte ^ b.getbyte(i) }
|
|
53
|
+
diff.zero?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Error < StandardError
|
|
57
|
+
attr_reader :status, :body
|
|
58
|
+
|
|
59
|
+
def initialize(status, message, body = nil)
|
|
60
|
+
super(message)
|
|
61
|
+
@status = status
|
|
62
|
+
@body = body
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Client
|
|
67
|
+
VERBS = {
|
|
68
|
+
"GET" => Net::HTTP::Get,
|
|
69
|
+
"POST" => Net::HTTP::Post,
|
|
70
|
+
"PUT" => Net::HTTP::Put,
|
|
71
|
+
"DELETE" => Net::HTTP::Delete,
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
def initialize(api_key, base_url = DEFAULT_BASE_URL)
|
|
75
|
+
@api_key = api_key
|
|
76
|
+
@base_url = base_url.sub(%r{/+\z}, "")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Low-level request. Every method below is a one-liner on top of this.
|
|
80
|
+
def request(method, path, body = nil)
|
|
81
|
+
uri = URI(@base_url + path)
|
|
82
|
+
req = VERBS.fetch(method).new(uri)
|
|
83
|
+
req["Authorization"] = "Bearer #{@api_key}"
|
|
84
|
+
unless body.nil?
|
|
85
|
+
req["Content-Type"] = "application/json"
|
|
86
|
+
req.body = JSON.generate(body)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
|
|
90
|
+
text = res.body
|
|
91
|
+
data = text && !text.empty? ? JSON.parse(text) : nil
|
|
92
|
+
code = res.code.to_i
|
|
93
|
+
unless code >= 200 && code < 300
|
|
94
|
+
message = data.is_a?(Hash) ? data["error"] : nil
|
|
95
|
+
raise Error.new(code, message || res.message || "HTTP #{code}", data)
|
|
96
|
+
end
|
|
97
|
+
data
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# --- Sending --------------------------------------------------------
|
|
101
|
+
def send(message)
|
|
102
|
+
request("POST", "/v1/send", message)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# --- Domains --------------------------------------------------------
|
|
106
|
+
def listDomains
|
|
107
|
+
request("GET", "/api/domains")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def createDomain(body)
|
|
111
|
+
request("POST", "/api/domains", body)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def getDomain(id)
|
|
115
|
+
request("GET", "/api/domains/#{id}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def deleteDomain(id)
|
|
119
|
+
request("DELETE", "/api/domains/#{id}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def verifyDomain(id)
|
|
123
|
+
request("POST", "/api/domains/#{id}/verify")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def setWebhook(id, body)
|
|
127
|
+
request("PUT", "/api/domains/#{id}/webhook", body)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def deleteWebhook(id)
|
|
131
|
+
request("DELETE", "/api/domains/#{id}/webhook")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def testWebhook(id)
|
|
135
|
+
request("POST", "/api/domains/#{id}/webhook/test")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# --- Routes ---------------------------------------------------------
|
|
139
|
+
def listRoutes
|
|
140
|
+
request("GET", "/api/routes")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def createRoute(body)
|
|
144
|
+
request("POST", "/api/routes", body)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# --- Messages & deliveries -----------------------------------------
|
|
148
|
+
def listMessages
|
|
149
|
+
request("GET", "/api/messages")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def getMessage(id)
|
|
153
|
+
request("GET", "/api/messages/#{id}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def retryDelivery(id)
|
|
157
|
+
request("POST", "/api/deliveries/#{id}/retry")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# --- Webhooks -------------------------------------------------------
|
|
161
|
+
# Instance wrapper around Mailkite.verify_webhook, so you can verify on an
|
|
162
|
+
# existing client. No network call; no API key required.
|
|
163
|
+
def verifyWebhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS)
|
|
164
|
+
Mailkite.verify_webhook(signature, payload, secret, tolerance_ms)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mailkite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- MailKite
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Send and manage email over your own authenticated domain with MailKite.
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/mailkite.rb
|
|
20
|
+
homepage: https://mailkite.dev/docs/libraries
|
|
21
|
+
licenses:
|
|
22
|
+
- MIT
|
|
23
|
+
metadata:
|
|
24
|
+
source_code_uri: https://github.com/fijiwebdesign/mailkite
|
|
25
|
+
post_install_message:
|
|
26
|
+
rdoc_options: []
|
|
27
|
+
require_paths:
|
|
28
|
+
- lib
|
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.5'
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
requirements: []
|
|
40
|
+
rubygems_version: 3.0.3.1
|
|
41
|
+
signing_key:
|
|
42
|
+
specification_version: 4
|
|
43
|
+
summary: Official MailKite SDK for Ruby
|
|
44
|
+
test_files: []
|