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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mailkite