mailkite-dev 0.3.0__py3-none-any.whl
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.
mailkite/__init__.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""MailKite SDK for Python.
|
|
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
|
+
from mailkite import MailKite
|
|
7
|
+
mk = MailKite(os.environ["MAILKITE_API_KEY"])
|
|
8
|
+
res = mk.send({"from": ..., "to": ..., "subject": ..., "text": ...})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import hmac
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.mailkite.dev"
|
|
20
|
+
# Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
|
|
21
|
+
DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
|
|
22
|
+
|
|
23
|
+
__all__ = ["MailKite", "MailKiteError", "verify_webhook"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def verify_webhook(signature, payload, secret, toleranceMs=DEFAULT_TOLERANCE_MS):
|
|
27
|
+
"""Verify an ``x-mailkite-signature`` header on an inbound webhook delivery.
|
|
28
|
+
|
|
29
|
+
Local HMAC-SHA256 check — no network call. Pass the raw, unparsed body
|
|
30
|
+
(``str`` or ``bytes``); the signature is over the exact bytes received.
|
|
31
|
+
Returns ``True`` only when the signature matches and the event is fresh.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(signature, str) or not signature:
|
|
34
|
+
return False
|
|
35
|
+
parts = {}
|
|
36
|
+
for seg in signature.split(","):
|
|
37
|
+
if "=" in seg:
|
|
38
|
+
k, v = seg.split("=", 1)
|
|
39
|
+
parts[k.strip()] = v.strip()
|
|
40
|
+
t = parts.get("t")
|
|
41
|
+
v1 = parts.get("v1")
|
|
42
|
+
if not t or not v1 or not t.lstrip("-").isdigit():
|
|
43
|
+
return False
|
|
44
|
+
# The t in the header is milliseconds since the epoch.
|
|
45
|
+
if toleranceMs and toleranceMs > 0:
|
|
46
|
+
if abs(time.time() * 1000 - int(t)) > toleranceMs:
|
|
47
|
+
return False
|
|
48
|
+
body = payload.encode("utf-8") if isinstance(payload, str) else payload
|
|
49
|
+
signed = (t + ".").encode("utf-8") + body
|
|
50
|
+
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
|
|
51
|
+
return hmac.compare_digest(expected, v1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MailKiteError(Exception):
|
|
55
|
+
def __init__(self, status, message, body=None):
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.status = status
|
|
58
|
+
self.message = message
|
|
59
|
+
self.body = body
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MailKite:
|
|
63
|
+
def __init__(self, apiKey, baseUrl=DEFAULT_BASE_URL):
|
|
64
|
+
self.apiKey = apiKey
|
|
65
|
+
self.baseUrl = baseUrl.rstrip("/")
|
|
66
|
+
|
|
67
|
+
# Low-level request. Every method below is a one-liner on top of this.
|
|
68
|
+
def request(self, method, path, body=None):
|
|
69
|
+
headers = {"Authorization": "Bearer " + self.apiKey}
|
|
70
|
+
data = None
|
|
71
|
+
if body is not None:
|
|
72
|
+
data = json.dumps(body).encode("utf-8")
|
|
73
|
+
headers["Content-Type"] = "application/json"
|
|
74
|
+
req = urllib.request.Request(self.baseUrl + path, data=data, method=method, headers=headers)
|
|
75
|
+
try:
|
|
76
|
+
with urllib.request.urlopen(req) as resp:
|
|
77
|
+
text = resp.read().decode("utf-8")
|
|
78
|
+
return json.loads(text) if text else None
|
|
79
|
+
except urllib.error.HTTPError as e:
|
|
80
|
+
text = e.read().decode("utf-8")
|
|
81
|
+
parsed = json.loads(text) if text else None
|
|
82
|
+
message = parsed.get("error") if isinstance(parsed, dict) else None
|
|
83
|
+
raise MailKiteError(e.code, message or e.reason or "HTTP %d" % e.code, parsed)
|
|
84
|
+
|
|
85
|
+
# --- Sending ----------------------------------------------------------
|
|
86
|
+
def send(self, message):
|
|
87
|
+
"""Send an email. ``message`` is a dict with ``from``, ``to`` and a
|
|
88
|
+
body (``text`` and/or ``html``). ``subject`` is optional — it may come
|
|
89
|
+
from a template. Pass ``templateId`` (str) to render a stored template
|
|
90
|
+
and ``templateData`` (dict) to supply its variables."""
|
|
91
|
+
return self.request("POST", "/v1/send", message)
|
|
92
|
+
|
|
93
|
+
def agent(self, message):
|
|
94
|
+
"""Send a message to an AI agent inbox. ``message`` is a dict with
|
|
95
|
+
``text`` (required) and optional ``subject``, ``from``, ``html``,
|
|
96
|
+
``routeId``, ``address`` and ``model``."""
|
|
97
|
+
return self.request("POST", "/v1/agent", message)
|
|
98
|
+
|
|
99
|
+
def route(self, message):
|
|
100
|
+
"""Route a message. ``message`` is a dict with ``from`` (required) and
|
|
101
|
+
optional ``routeId``, ``address``, ``subject``, ``text`` and ``html``."""
|
|
102
|
+
return self.request("POST", "/v1/route", message)
|
|
103
|
+
|
|
104
|
+
# --- Templates --------------------------------------------------------
|
|
105
|
+
def listTemplates(self):
|
|
106
|
+
return self.request("GET", "/api/templates")
|
|
107
|
+
|
|
108
|
+
def listBaseTemplates(self):
|
|
109
|
+
return self.request("GET", "/api/templates/base")
|
|
110
|
+
|
|
111
|
+
def getTemplate(self, id):
|
|
112
|
+
return self.request("GET", "/api/templates/%s" % id)
|
|
113
|
+
|
|
114
|
+
def createTemplate(self, body):
|
|
115
|
+
return self.request("POST", "/api/templates", body)
|
|
116
|
+
|
|
117
|
+
# --- Domains ----------------------------------------------------------
|
|
118
|
+
def listDomains(self):
|
|
119
|
+
return self.request("GET", "/api/domains")
|
|
120
|
+
|
|
121
|
+
def createDomain(self, body):
|
|
122
|
+
return self.request("POST", "/api/domains", body)
|
|
123
|
+
|
|
124
|
+
def getDomain(self, id):
|
|
125
|
+
return self.request("GET", "/api/domains/%s" % id)
|
|
126
|
+
|
|
127
|
+
def deleteDomain(self, id):
|
|
128
|
+
return self.request("DELETE", "/api/domains/%s" % id)
|
|
129
|
+
|
|
130
|
+
def verifyDomain(self, id):
|
|
131
|
+
return self.request("POST", "/api/domains/%s/verify" % id)
|
|
132
|
+
|
|
133
|
+
def setWebhook(self, id, body):
|
|
134
|
+
return self.request("PUT", "/api/domains/%s/webhook" % id, body)
|
|
135
|
+
|
|
136
|
+
def deleteWebhook(self, id):
|
|
137
|
+
return self.request("DELETE", "/api/domains/%s/webhook" % id)
|
|
138
|
+
|
|
139
|
+
def testWebhook(self, id):
|
|
140
|
+
return self.request("POST", "/api/domains/%s/webhook/test" % id)
|
|
141
|
+
|
|
142
|
+
def checkDomainAvailability(self, domain):
|
|
143
|
+
return self.request("GET", "/api/domains/register/check?domain=%s" % urllib.parse.quote(domain))
|
|
144
|
+
|
|
145
|
+
def registerDomain(self, body):
|
|
146
|
+
return self.request("POST", "/api/domains/register", body)
|
|
147
|
+
|
|
148
|
+
# --- Routes -----------------------------------------------------------
|
|
149
|
+
def listRoutes(self):
|
|
150
|
+
return self.request("GET", "/api/routes")
|
|
151
|
+
|
|
152
|
+
def createRoute(self, body):
|
|
153
|
+
return self.request("POST", "/api/routes", body)
|
|
154
|
+
|
|
155
|
+
# --- Messages & deliveries -------------------------------------------
|
|
156
|
+
def listMessages(self):
|
|
157
|
+
return self.request("GET", "/api/messages")
|
|
158
|
+
|
|
159
|
+
def getMessage(self, id):
|
|
160
|
+
return self.request("GET", "/api/messages/%s" % id)
|
|
161
|
+
|
|
162
|
+
def retryDelivery(self, id):
|
|
163
|
+
return self.request("POST", "/api/deliveries/%s/retry" % id)
|
|
164
|
+
|
|
165
|
+
# --- Webhooks ---------------------------------------------------------
|
|
166
|
+
def verifyWebhook(self, signature, payload, secret, toleranceMs=DEFAULT_TOLERANCE_MS):
|
|
167
|
+
"""Verify an ``x-mailkite-signature`` header. See module-level
|
|
168
|
+
:func:`verify_webhook` — this is a thin instance wrapper, so you can
|
|
169
|
+
call it on an existing client without re-importing."""
|
|
170
|
+
return verify_webhook(signature, payload, secret, toleranceMs)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailkite-dev
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official MailKite SDK for Python — send and manage email over your own authenticated domain.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://mailkite.dev/docs/libraries
|
|
7
|
+
Project-URL: Repository, https://github.com/mailkite/mailkite
|
|
8
|
+
Keywords: mailkite,email,transactional,api
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# MailKite for Python
|
|
13
|
+
|
|
14
|
+
Official [MailKite](https://mailkite.dev) SDK. One low-level `request()` plus one
|
|
15
|
+
method per endpoint. Zero dependencies — standard library only. Python 3.7+.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install mailkite-dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> Published as `mailkite-dev` for now (the `mailkite` name is being reclaimed). The
|
|
24
|
+
> import is unchanged — `from mailkite import MailKite`.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from mailkite import MailKite
|
|
31
|
+
|
|
32
|
+
mk = MailKite(os.environ["MAILKITE_API_KEY"])
|
|
33
|
+
|
|
34
|
+
res = mk.send({
|
|
35
|
+
"from": "hello@myapp.ai",
|
|
36
|
+
"to": "ada@example.com",
|
|
37
|
+
"subject": "Your invoice #1042",
|
|
38
|
+
"html": "<p>Thanks! Receipt attached.</p>",
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Point at a different base URL with `MailKite(key, "https://api.mailkite.dev")`.
|
|
43
|
+
|
|
44
|
+
## Methods
|
|
45
|
+
|
|
46
|
+
`send(message)`, `agent(message)`, `route(message)`, `listDomains()`, `createDomain({"domain": ...})`,
|
|
47
|
+
`getDomain(id)`, `deleteDomain(id)`, `verifyDomain(id)`,
|
|
48
|
+
`setWebhook(id, {"url": ...})`, `deleteWebhook(id)`, `testWebhook(id)`,
|
|
49
|
+
`checkDomainAvailability(domain)`, `registerDomain({"domain": ..., "contact": {...}})`,
|
|
50
|
+
`listRoutes()`, `createRoute({...})`, `listMessages()`, `getMessage(id)`,
|
|
51
|
+
`retryDelivery(id)`.
|
|
52
|
+
|
|
53
|
+
## Errors
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from mailkite import MailKiteError
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
mk.send(msg)
|
|
60
|
+
except MailKiteError as e:
|
|
61
|
+
print(e.status, e.message)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
See the [full docs](https://mailkite.dev/docs/libraries).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
mailkite/__init__.py,sha256=nQMshndS88_H0rgQFlvjhkiVAHm0qjLxmIVB72XNmcg,6765
|
|
2
|
+
mailkite_dev-0.3.0.dist-info/METADATA,sha256=Gq-_oVxgxpRLgoyQg7KU8nWG-9_3JLejXNb_dopk0Cs,1765
|
|
3
|
+
mailkite_dev-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
mailkite_dev-0.3.0.dist-info/top_level.txt,sha256=bc29TJ7KmxbFePYiquHfd7teobuAkjxFvtEJjk28QGk,9
|
|
5
|
+
mailkite_dev-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mailkite
|