mailkite 0.2.0 → 0.4.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/lib/mailkite.rb +106 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf95721d75808a7648a0d5c5528a5b24cebe649e4e7b4f2aa7cf96309492c4df
|
|
4
|
+
data.tar.gz: fd40da8370ad8ad24b538613a29a8b623226901a8db8e5bc6a4b0b3ebd1ec709
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96e4a3c87afd08886338448d1edf8bfd83d7433733bba220cb892647249c923d46dde87df440ce78c84f1301b734fec480b89d9ee2e54f8eef2e528fec4af4dc
|
|
7
|
+
data.tar.gz: d7b4ccb09e09a382e7d9bef520115c50aa06c635646763b73cea18bc133326b6080371a56037b3f05ccb5461b53b9d2369d4f5bfde84dedd1e5770d0fe0443d2
|
data/lib/mailkite.rb
CHANGED
|
@@ -11,10 +11,11 @@ require "net/http"
|
|
|
11
11
|
require "uri"
|
|
12
12
|
require "json"
|
|
13
13
|
require "openssl"
|
|
14
|
+
require "base64"
|
|
14
15
|
require "cgi"
|
|
15
16
|
|
|
16
17
|
module Mailkite
|
|
17
|
-
VERSION = "0.
|
|
18
|
+
VERSION = "0.4.0"
|
|
18
19
|
DEFAULT_BASE_URL = "https://api.mailkite.dev"
|
|
19
20
|
# Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
|
|
20
21
|
DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
|
|
@@ -54,6 +55,65 @@ module Mailkite
|
|
|
54
55
|
diff.zero?
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# The canonical body to return from a webhook handler so MailKite marks the
|
|
59
|
+
# delivery acknowledged. Returns the exact string `{"status":"ok"}`.
|
|
60
|
+
def self.reply_ok
|
|
61
|
+
'{"status":"ok"}'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Encrypt a UTF-8 string to a customer RSA public key (SPKI/PEM), producing the
|
|
65
|
+
# MailKite at-rest envelope as a compact JSON string. Hybrid scheme: a fresh
|
|
66
|
+
# AES-256-GCM content key encrypts the data, then the raw key is wrapped with
|
|
67
|
+
# RSA-OAEP (SHA-256). Local only — no network.
|
|
68
|
+
def self.encrypt(plaintext, public_key)
|
|
69
|
+
pub = OpenSSL::PKey.read(public_key)
|
|
70
|
+
fp = OpenSSL::Digest::SHA256.hexdigest(pub.to_der)
|
|
71
|
+
|
|
72
|
+
raw_key = OpenSSL::Random.random_bytes(32)
|
|
73
|
+
iv = OpenSSL::Random.random_bytes(12)
|
|
74
|
+
|
|
75
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
76
|
+
cipher.encrypt
|
|
77
|
+
cipher.key = raw_key
|
|
78
|
+
cipher.iv = iv
|
|
79
|
+
body = cipher.update(plaintext.to_s.dup.force_encoding("UTF-8")) + cipher.final
|
|
80
|
+
ciphertext = body + cipher.auth_tag # GCM ct with 16-byte tag appended
|
|
81
|
+
|
|
82
|
+
wrapped = pub.encrypt(raw_key, rsa_padding_mode: "oaep", rsa_oaep_md: "sha256", rsa_mgf1_md: "sha256")
|
|
83
|
+
|
|
84
|
+
JSON.generate({
|
|
85
|
+
"v" => 1,
|
|
86
|
+
"keyAlg" => "RSA-OAEP-256",
|
|
87
|
+
"fp" => fp,
|
|
88
|
+
"enc" => "A256GCM",
|
|
89
|
+
"iv" => Base64.strict_encode64(iv),
|
|
90
|
+
"wrappedKey" => Base64.strict_encode64(wrapped),
|
|
91
|
+
"ciphertext" => Base64.strict_encode64(ciphertext),
|
|
92
|
+
})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Decrypt a MailKite at-rest envelope (JSON string) with the matching RSA
|
|
96
|
+
# private key (PEM), returning the original UTF-8 plaintext. Local only.
|
|
97
|
+
def self.decrypt(envelope, private_key)
|
|
98
|
+
env = envelope.is_a?(String) ? JSON.parse(envelope) : envelope
|
|
99
|
+
priv = OpenSSL::PKey.read(private_key)
|
|
100
|
+
|
|
101
|
+
iv = Base64.strict_decode64(env["iv"])
|
|
102
|
+
wrapped = Base64.strict_decode64(env["wrappedKey"])
|
|
103
|
+
ct = Base64.strict_decode64(env["ciphertext"])
|
|
104
|
+
body = ct[0...-16]
|
|
105
|
+
tag = ct[-16..]
|
|
106
|
+
|
|
107
|
+
raw_key = priv.decrypt(wrapped, rsa_padding_mode: "oaep", rsa_oaep_md: "sha256", rsa_mgf1_md: "sha256")
|
|
108
|
+
|
|
109
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
110
|
+
cipher.decrypt
|
|
111
|
+
cipher.key = raw_key
|
|
112
|
+
cipher.iv = iv
|
|
113
|
+
cipher.auth_tag = tag
|
|
114
|
+
(cipher.update(body) + cipher.final).force_encoding("UTF-8")
|
|
115
|
+
end
|
|
116
|
+
|
|
57
117
|
class Error < StandardError
|
|
58
118
|
attr_reader :status, :body
|
|
59
119
|
|
|
@@ -99,10 +159,25 @@ module Mailkite
|
|
|
99
159
|
end
|
|
100
160
|
|
|
101
161
|
# --- Sending --------------------------------------------------------
|
|
162
|
+
# message keys: from, to, text/html, etc. `subject` is optional when a
|
|
163
|
+
# template supplies it. Pass `templateId` to render a stored template and
|
|
164
|
+
# `templateData` (a hash) to fill its variables.
|
|
102
165
|
def send(message)
|
|
103
166
|
request("POST", "/v1/send", message)
|
|
104
167
|
end
|
|
105
168
|
|
|
169
|
+
# Run an inbound message through an AI agent. message keys: text (required),
|
|
170
|
+
# plus optional subject/from/html/routeId/address/model.
|
|
171
|
+
def agent(message)
|
|
172
|
+
request("POST", "/v1/agent", message)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Route an inbound message to its configured destination. message keys: from
|
|
176
|
+
# (required), plus optional routeId/address/subject/text/html.
|
|
177
|
+
def route(message)
|
|
178
|
+
request("POST", "/v1/route", message)
|
|
179
|
+
end
|
|
180
|
+
|
|
106
181
|
# --- Domains --------------------------------------------------------
|
|
107
182
|
def listDomains
|
|
108
183
|
request("GET", "/api/domains")
|
|
@@ -153,6 +228,23 @@ module Mailkite
|
|
|
153
228
|
request("POST", "/api/routes", body)
|
|
154
229
|
end
|
|
155
230
|
|
|
231
|
+
# --- Templates ------------------------------------------------------
|
|
232
|
+
def listTemplates
|
|
233
|
+
request("GET", "/api/templates")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def listBaseTemplates
|
|
237
|
+
request("GET", "/api/templates/base")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def getTemplate(id)
|
|
241
|
+
request("GET", "/api/templates/#{id}")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def createTemplate(body)
|
|
245
|
+
request("POST", "/api/templates", body)
|
|
246
|
+
end
|
|
247
|
+
|
|
156
248
|
# --- Messages & deliveries -----------------------------------------
|
|
157
249
|
def listMessages
|
|
158
250
|
request("GET", "/api/messages")
|
|
@@ -172,5 +264,18 @@ module Mailkite
|
|
|
172
264
|
def verifyWebhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS)
|
|
173
265
|
Mailkite.verify_webhook(signature, payload, secret, tolerance_ms)
|
|
174
266
|
end
|
|
267
|
+
|
|
268
|
+
# Instance wrappers around the module-level crypto helpers. No network call.
|
|
269
|
+
def reply_ok
|
|
270
|
+
Mailkite.reply_ok
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def encrypt(plaintext, public_key)
|
|
274
|
+
Mailkite.encrypt(plaintext, public_key)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def decrypt(envelope, private_key)
|
|
278
|
+
Mailkite.decrypt(envelope, private_key)
|
|
279
|
+
end
|
|
175
280
|
end
|
|
176
281
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailkite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- MailKite
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Send and manage email over your own authenticated domain with MailKite.
|
|
14
14
|
email:
|
|
@@ -21,7 +21,7 @@ homepage: https://mailkite.dev/docs/libraries
|
|
|
21
21
|
licenses:
|
|
22
22
|
- MIT
|
|
23
23
|
metadata:
|
|
24
|
-
source_code_uri: https://github.com/
|
|
24
|
+
source_code_uri: https://github.com/mailkite/mailkite
|
|
25
25
|
post_install_message:
|
|
26
26
|
rdoc_options: []
|
|
27
27
|
require_paths:
|