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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mailkite.rb +167 -0
  3. 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: []