gork-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: gork-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for gork.email programmable email infrastructure for AI agents
5
+ Author: gork.email
6
+ License: MIT
7
+ Project-URL: Homepage, https://gork.email
8
+ Project-URL: Documentation, https://gork.email/docs
9
+ Project-URL: Repository, https://github.com/shyamcxy/gork-sdk-python
10
+ Project-URL: Issues, https://github.com/shyamcxy/gork-sdk-python/issues
11
+ Keywords: email,agent,ai,mcp,inbox,mail
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Communications :: Email
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+
19
+ # gork-sdk — Python SDK for gork.email
20
+
21
+ > **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
22
+
23
+ Programmable email infrastructure for AI agents. Provision inboxes, send and
24
+ receive mail, search a mailbox, compose drafts, schedule sends, read
25
+ attachments, and manage threads, webhooks, domains, keys, and suppressions.
26
+
27
+ This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
28
+ SDK (`@gork/sdk`); the method names and parameters match.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install gork-sdk
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from gork import GorkClient
40
+
41
+ gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
42
+
43
+ inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
44
+ print(inbox["address"]) # sdr-alex@try.gork.email
45
+
46
+ gork.send_message(
47
+ inbox_id=inbox["id"],
48
+ to=["prospect@acme.com"],
49
+ subject="Quick intro",
50
+ text="Hi — wanted to share what we're building.",
51
+ idempotency_key="outreach-001", # safe retries
52
+ )
53
+ ```
54
+
55
+ ## Webhook verification
56
+
57
+ ```python
58
+ from gork import verify_signature
59
+
60
+ ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
61
+ ```
62
+
63
+ ## Links
64
+
65
+ - Docs: https://gork.email/docs
66
+ - API reference: https://api.gork.email/openapi.json
67
+ - Changelog: https://gork.email/changelog
@@ -0,0 +1,49 @@
1
+ # gork-sdk — Python SDK for gork.email
2
+
3
+ > **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
4
+
5
+ Programmable email infrastructure for AI agents. Provision inboxes, send and
6
+ receive mail, search a mailbox, compose drafts, schedule sends, read
7
+ attachments, and manage threads, webhooks, domains, keys, and suppressions.
8
+
9
+ This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
10
+ SDK (`@gork/sdk`); the method names and parameters match.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install gork-sdk
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from gork import GorkClient
22
+
23
+ gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
24
+
25
+ inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
26
+ print(inbox["address"]) # sdr-alex@try.gork.email
27
+
28
+ gork.send_message(
29
+ inbox_id=inbox["id"],
30
+ to=["prospect@acme.com"],
31
+ subject="Quick intro",
32
+ text="Hi — wanted to share what we're building.",
33
+ idempotency_key="outreach-001", # safe retries
34
+ )
35
+ ```
36
+
37
+ ## Webhook verification
38
+
39
+ ```python
40
+ from gork import verify_signature
41
+
42
+ ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
43
+ ```
44
+
45
+ ## Links
46
+
47
+ - Docs: https://gork.email/docs
48
+ - API reference: https://api.gork.email/openapi.json
49
+ - Changelog: https://gork.email/changelog
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gork-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for gork.email programmable email infrastructure for AI agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "gork.email" }]
13
+ keywords = ["email", "agent", "ai", "mcp", "inbox", "mail"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Topic :: Communications :: Email",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://gork.email"
23
+ Documentation = "https://gork.email/docs"
24
+ Repository = "https://github.com/shyamcxy/gork-sdk-python"
25
+ Issues = "https://github.com/shyamcxy/gork-sdk-python/issues"
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ """gork.email Python SDK — programmable email infrastructure for AI agents."""
2
+
3
+ from .client import GorkClient
4
+ from .errors import GorkError
5
+ from .types import (
6
+ Inbox,
7
+ Message,
8
+ Thread,
9
+ Webhook,
10
+ ApiKey,
11
+ Domain,
12
+ Suppression,
13
+ )
14
+
15
+ Gork = GorkClient
16
+
17
+ __all__ = [
18
+ "Gork",
19
+ "GorkClient",
20
+ "GorkError",
21
+ "Inbox",
22
+ "Message",
23
+ "Thread",
24
+ "Webhook",
25
+ "ApiKey",
26
+ "Domain",
27
+ "Suppression",
28
+ ]
29
+
30
+ __version__ = "0.1.0"
@@ -0,0 +1,468 @@
1
+ """GorkClient — zero-dependency (stdlib only) REST client for api.gork.email."""
2
+
3
+ import hashlib
4
+ import hmac
5
+ import time
6
+ import json
7
+ import os
8
+ import urllib.parse
9
+ import urllib.request
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from .errors import GorkError
13
+
14
+ DEFAULT_BASE_URL = "https://api.gork.email"
15
+
16
+
17
+ def verify_signature(
18
+ payload: str, signature_header: str, secret: str, tolerance_seconds: int = 300
19
+ ) -> bool:
20
+ """Verify an X-Gork-Signature webhook header (HMAC-SHA256).
21
+
22
+ Rejects signatures older than ``tolerance_seconds`` (default 300s,
23
+ matching the TypeScript verifiers) to prevent replay attacks.
24
+ """
25
+ try:
26
+ parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
27
+ timestamp, signature = parts.get("t", ""), parts.get("v1", "")
28
+ if not timestamp or not signature:
29
+ return False
30
+ # Freshness check — a captured payload+signature must not verify
31
+ # forever (previously there was no tolerance window at all).
32
+ if abs(time.time() - int(timestamp)) > tolerance_seconds:
33
+ return False
34
+ expected = hmac.new(
35
+ secret.encode(), f"{timestamp}.{payload}".encode(), hashlib.sha256
36
+ ).hexdigest()
37
+ return hmac.compare_digest(signature, expected)
38
+ except Exception:
39
+ return False
40
+
41
+
42
+ def _client_version() -> str:
43
+ """Version reported in the User-Agent header.
44
+
45
+ Read from the installed distribution metadata so it always matches the
46
+ released version; falls back to the source version when running from a
47
+ checkout that was never installed.
48
+ """
49
+ try:
50
+ from importlib.metadata import version as _v
51
+
52
+ return _v("gork-sdk")
53
+ except Exception:
54
+ return "0.1.0"
55
+
56
+
57
+ class GorkClient:
58
+ def __init__(self, api_key: str = "", base_url: str = DEFAULT_BASE_URL):
59
+ self.api_key = api_key or os.environ.get("GORK_KEY") or os.environ.get("GORK_API_KEY") or ""
60
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
61
+ if not self.api_key:
62
+ import warnings
63
+
64
+ warnings.warn(
65
+ "[gork] initialized without an api_key. Set GORK_KEY or pass api_key explicitly."
66
+ )
67
+
68
+ def _request(
69
+ self,
70
+ method: str,
71
+ path: str,
72
+ body: Optional[Dict[str, Any]] = None,
73
+ query: Optional[Dict[str, Any]] = None,
74
+ headers: Optional[Dict[str, str]] = None,
75
+ ) -> Any:
76
+ url = self.base_url + path
77
+ if query:
78
+ url += "?" + urllib.parse.urlencode({k: v for k, v in query.items() if v is not None})
79
+ data = json.dumps(body).encode() if body is not None else None
80
+ req = urllib.request.Request(url, data=data, method=method)
81
+ req.add_header("Authorization", f"Bearer {self.api_key}")
82
+ req.add_header("Content-Type", "application/json")
83
+ req.add_header("User-Agent", f"gork-sdk-python/{_client_version()}")
84
+ for k, v in (headers or {}).items():
85
+ req.add_header(k, v)
86
+ try:
87
+ with urllib.request.urlopen(req, timeout=30) as res:
88
+ payload = json.loads(res.read().decode() or "{}")
89
+ except urllib.error.HTTPError as e:
90
+ try:
91
+ payload = json.loads(e.read().decode() or "{}")
92
+ except Exception:
93
+ payload = {}
94
+ err = (payload.get("error") or {}) if isinstance(payload, dict) else {}
95
+ raise GorkError(
96
+ err.get("message") or f"API request failed with status {e.code}",
97
+ err.get("code") or "api_error",
98
+ e.code,
99
+ err.get("details"),
100
+ )
101
+ data_key = payload.get("data", payload) if isinstance(payload, dict) else payload
102
+ return data_key
103
+
104
+ def _request_bytes(self, path: str, inline: bool = False) -> bytes:
105
+ """Fetch a binary endpoint (attachment bytes) rather than JSON."""
106
+ url = self.base_url + path
107
+ if inline:
108
+ url += "?inline=1"
109
+ req = urllib.request.Request(url, method="GET")
110
+ req.add_header("Authorization", f"Bearer {self.api_key}")
111
+ req.add_header("User-Agent", f"gork-sdk-python/{_client_version()}")
112
+ try:
113
+ with urllib.request.urlopen(req, timeout=60) as res:
114
+ return res.read()
115
+ except urllib.error.HTTPError as e:
116
+ try:
117
+ payload = json.loads(e.read().decode() or "{}")
118
+ except Exception:
119
+ payload = {}
120
+ err = (payload.get("error") or {}) if isinstance(payload, dict) else {}
121
+ raise GorkError(
122
+ err.get("message") or f"API request failed with status {e.code}",
123
+ err.get("code") or "api_error",
124
+ e.code,
125
+ err.get("details"),
126
+ )
127
+
128
+ # ---- Inboxes ----
129
+ def create_inbox(self, username: str = "", name: str = "") -> Dict[str, Any]:
130
+ return self._request("POST", "/v1/inboxes", {"username": username, "name": name})
131
+
132
+ def list_inboxes(self) -> List[Dict[str, Any]]:
133
+ return self._request("GET", "/v1/inboxes")
134
+
135
+ def get_inbox(self, inbox_id: str) -> Dict[str, Any]:
136
+ return self._request("GET", f"/v1/inboxes/{urllib.parse.quote(inbox_id)}")
137
+
138
+ def delete_inbox(self, inbox_id: str) -> Dict[str, Any]:
139
+ return self._request("DELETE", f"/v1/inboxes/{urllib.parse.quote(inbox_id)}")
140
+
141
+ # ---- Messages ----
142
+ def send_message(
143
+ self,
144
+ inbox_id: str,
145
+ to: List[str],
146
+ subject: str = "(no subject)",
147
+ text: str = "",
148
+ html: str = "",
149
+ in_reply_to: str = "",
150
+ idempotency_key: str = "",
151
+ attachments: Optional[List[Dict[str, Any]]] = None,
152
+ send_at: str = "",
153
+ track_opens: bool = False,
154
+ ) -> Dict[str, Any]:
155
+ """Send (or schedule) an outbound email.
156
+
157
+ attachments: [{"filename": "invoice.pdf", "contentType": "application/pdf",
158
+ "content": "<base64>"}] — max 10 files, 10MB each, 15MB total.
159
+ send_at: ISO 8601 timestamp to schedule instead of sending now
160
+ (1 minute to 30 days ahead). No quota is used until it dispatches.
161
+ track_opens: opt in to open tracking (adds a signed pixel, fires
162
+ email.opened on the first open).
163
+ """
164
+ body: Dict[str, Any] = {"inboxId": inbox_id, "to": to, "subject": subject}
165
+ if text:
166
+ body["text"] = text
167
+ if html:
168
+ body["html"] = html
169
+ if in_reply_to:
170
+ body["inReplyTo"] = in_reply_to
171
+ if attachments:
172
+ body["attachments"] = attachments
173
+ if send_at:
174
+ body["sendAt"] = send_at
175
+ if track_opens:
176
+ body["trackOpens"] = True
177
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
178
+ return self._request("POST", "/v1/messages/send", body, headers=headers)
179
+
180
+ def cancel_scheduled(self, message_id: str) -> Dict[str, Any]:
181
+ """Cancel a scheduled send before it dispatches."""
182
+ return self._request(
183
+ "DELETE", f"/v1/messages/{urllib.parse.quote(message_id)}/schedule"
184
+ )
185
+
186
+ def list_messages(
187
+ self,
188
+ inbox_id: str = "",
189
+ thread_id: str = "",
190
+ limit: int = 50,
191
+ q: str = "",
192
+ direction: str = "",
193
+ from_address: str = "",
194
+ since: str = "",
195
+ until: str = "",
196
+ ) -> List[Dict[str, Any]]:
197
+ return self._request(
198
+ "GET", "/v1/messages",
199
+ query={
200
+ "inboxId": inbox_id or None,
201
+ "threadId": thread_id or None,
202
+ "limit": limit,
203
+ "q": q or None,
204
+ "direction": direction or None,
205
+ "from": from_address or None,
206
+ "since": since or None,
207
+ "until": until or None,
208
+ },
209
+ )
210
+
211
+ def search_messages(self, query: str, **filters: Any) -> List[Dict[str, Any]]:
212
+ """Search subject, body, sender and recipients (newest first)."""
213
+ return self.list_messages(q=query, **filters)
214
+
215
+ def download_attachment(self, attachment_id: str, inline: bool = False) -> bytes:
216
+ """Download an attachment's bytes. Ids come from a message's `attachments`."""
217
+ return self._request_bytes(
218
+ f"/v1/attachments/{urllib.parse.quote(attachment_id)}", inline=inline
219
+ )
220
+
221
+ # ---- Drafts (compose without sending; no quota used) ----
222
+ def create_draft(
223
+ self,
224
+ inbox_id: str,
225
+ to: Optional[List[str]] = None,
226
+ subject: str = "",
227
+ text: str = "",
228
+ html: str = "",
229
+ in_reply_to: str = "",
230
+ ) -> Dict[str, Any]:
231
+ body: Dict[str, Any] = {"inboxId": inbox_id}
232
+ if to:
233
+ body["to"] = to
234
+ if subject:
235
+ body["subject"] = subject
236
+ if text:
237
+ body["text"] = text
238
+ if html:
239
+ body["html"] = html
240
+ if in_reply_to:
241
+ body["inReplyTo"] = in_reply_to
242
+ return self._request("POST", "/v1/drafts", body)
243
+
244
+ def list_drafts(
245
+ self, inbox_id: str = "", status: str = "draft", limit: int = 50
246
+ ) -> List[Dict[str, Any]]:
247
+ return self._request(
248
+ "GET", "/v1/drafts",
249
+ query={
250
+ "inboxId": inbox_id or None,
251
+ "status": status or None,
252
+ "limit": limit,
253
+ },
254
+ )
255
+
256
+ def get_draft(self, draft_id: str) -> Dict[str, Any]:
257
+ return self._request("GET", f"/v1/drafts/{urllib.parse.quote(draft_id)}")
258
+
259
+ def update_draft(self, draft_id: str, **fields: Any) -> Dict[str, Any]:
260
+ """Update draft fields, e.g. update_draft(id, subject="Hi", to=["a@b.com"])."""
261
+ return self._request(
262
+ "PATCH", f"/v1/drafts/{urllib.parse.quote(draft_id)}", fields
263
+ )
264
+
265
+ def discard_draft(self, draft_id: str) -> Dict[str, Any]:
266
+ return self._request("DELETE", f"/v1/drafts/{urllib.parse.quote(draft_id)}")
267
+
268
+ def send_draft(self, draft_id: str) -> Dict[str, Any]:
269
+ """Send a draft through the full pipeline. Idempotent per draft."""
270
+ return self._request(
271
+ "POST", f"/v1/drafts/{urllib.parse.quote(draft_id)}/send"
272
+ )
273
+
274
+ def get_message(self, message_id: str) -> Dict[str, Any]:
275
+ return self._request("GET", f"/v1/messages/{urllib.parse.quote(message_id)}")
276
+
277
+ # ---- Threads ----
278
+ def list_threads(self) -> List[Dict[str, Any]]:
279
+ return self._request("GET", "/v1/threads")
280
+
281
+ def get_thread(self, thread_id: str) -> Dict[str, Any]:
282
+ return self._request("GET", f"/v1/threads/{urllib.parse.quote(thread_id)}")
283
+
284
+ # ---- Webhooks ----
285
+ def create_webhook(self, url: str, events: Optional[List[str]] = None) -> Dict[str, Any]:
286
+ return self._request(
287
+ "POST", "/v1/webhooks",
288
+ {"url": url, "subscribedEvents": events or ["email.received", "email.sent", "email.bounced"]},
289
+ )
290
+
291
+ def list_webhooks(self) -> List[Dict[str, Any]]:
292
+ return self._request("GET", "/v1/webhooks")
293
+
294
+ def rotate_webhook_secret(self, webhook_id: str) -> Dict[str, Any]:
295
+ return self._request("POST", f"/v1/webhooks/{urllib.parse.quote(webhook_id)}/rotate-secret")
296
+
297
+ def delete_webhook(self, webhook_id: str) -> Dict[str, Any]:
298
+ return self._request("DELETE", f"/v1/webhooks/{urllib.parse.quote(webhook_id)}")
299
+
300
+ def list_deliveries(self, limit: int = 50) -> List[Dict[str, Any]]:
301
+ return self._request("GET", "/v1/webhooks/deliveries", query={"limit": limit})
302
+
303
+ # ---- API keys ----
304
+ def create_api_key(self, name: str, scopes: Optional[List[str]] = None) -> Dict[str, Any]:
305
+ return self._request("POST", "/v1/keys", {"name": name, "scopes": scopes or ["*"]})
306
+
307
+ def list_api_keys(self) -> List[Dict[str, Any]]:
308
+ return self._request("GET", "/v1/keys")
309
+
310
+ def revoke_api_key(self, key_id: str) -> Dict[str, Any]:
311
+ return self._request("DELETE", f"/v1/keys/{urllib.parse.quote(key_id)}")
312
+
313
+ # ---- Domains ----
314
+ def create_domain(self, domain: str) -> Dict[str, Any]:
315
+ return self._request("POST", "/v1/domains", {"domain": domain})
316
+
317
+ def list_domains(self) -> List[Dict[str, Any]]:
318
+ return self._request("GET", "/v1/domains")
319
+
320
+ def verify_domain(self, domain_id: str) -> Dict[str, Any]:
321
+ return self._request("POST", f"/v1/domains/{urllib.parse.quote(domain_id)}/verify")
322
+
323
+ def delete_domain(self, domain_id: str) -> Dict[str, Any]:
324
+ return self._request("DELETE", f"/v1/domains/{urllib.parse.quote(domain_id)}")
325
+
326
+ # ---- Suppressions ----
327
+ def list_suppressions(self) -> List[Dict[str, Any]]:
328
+ return self._request("GET", "/v1/suppressions")
329
+
330
+ def suppress(self, email: str, reason: str = "manual") -> Dict[str, Any]:
331
+ return self._request("POST", "/v1/suppressions", {"email": email, "reason": reason})
332
+
333
+ def unsuppress(self, suppression_id: str) -> Dict[str, Any]:
334
+ return self._request("DELETE", f"/v1/suppressions/{urllib.parse.quote(suppression_id)}")
335
+
336
+ # ---- AI Agent Tools (OpenAI / Anthropic / LangChain / CrewAI compatible) ----
337
+ def get_tools(self) -> List[Dict[str, Any]]:
338
+ """Return standard function-calling tool schemas for OpenAI, Anthropic, or LangChain."""
339
+ return [
340
+ {
341
+ "type": "function",
342
+ "function": {
343
+ "name": "gork_create_inbox",
344
+ "description": "Provision a new programmable email inbox for an AI agent.",
345
+ "parameters": {
346
+ "type": "object",
347
+ "properties": {
348
+ "username": {"type": "string", "description": "Lowercase username, e.g. sdr-alex"},
349
+ "name": {"type": "string", "description": "Friendly agent display name"},
350
+ },
351
+ "required": ["username"],
352
+ },
353
+ },
354
+ },
355
+ {
356
+ "type": "function",
357
+ "function": {
358
+ "name": "gork_list_inboxes",
359
+ "description": "List all email inboxes available to the organization.",
360
+ "parameters": {"type": "object", "properties": {}},
361
+ },
362
+ },
363
+ {
364
+ "type": "function",
365
+ "function": {
366
+ "name": "gork_send_email",
367
+ "description": "Send an outbound email from an agent inbox.",
368
+ "parameters": {
369
+ "type": "object",
370
+ "properties": {
371
+ "inbox_id": {"type": "string", "description": "The ID of the inbox to send from"},
372
+ "to": {"type": "array", "items": {"type": "string"}, "description": "Recipient email addresses"},
373
+ "subject": {"type": "string", "description": "Email subject line"},
374
+ "text": {"type": "string", "description": "Plain text email body"},
375
+ },
376
+ "required": ["inbox_id", "to", "subject", "text"],
377
+ },
378
+ },
379
+ },
380
+ {
381
+ "type": "function",
382
+ "function": {
383
+ "name": "gork_read_emails",
384
+ "description": "List or search messages, optionally filtered by inbox.",
385
+ "parameters": {
386
+ "type": "object",
387
+ "properties": {
388
+ "inbox_id": {"type": "string", "description": "Filter by inbox ID"},
389
+ "limit": {"type": "integer", "description": "Max messages to return (default 20)"},
390
+ },
391
+ },
392
+ },
393
+ },
394
+ {
395
+ "type": "function",
396
+ "function": {
397
+ "name": "gork_get_message",
398
+ "description": "Retrieve full details and content of a specific email message.",
399
+ "parameters": {
400
+ "type": "object",
401
+ "properties": {
402
+ "message_id": {"type": "string", "description": "The ID of the message (msg_...)"},
403
+ },
404
+ "required": ["message_id"],
405
+ },
406
+ },
407
+ },
408
+ {
409
+ "type": "function",
410
+ "function": {
411
+ "name": "gork_create_draft",
412
+ "description": "Create an email draft for review before sending.",
413
+ "parameters": {
414
+ "type": "object",
415
+ "properties": {
416
+ "inbox_id": {"type": "string", "description": "Inbox ID"},
417
+ "to": {"type": "array", "items": {"type": "string"}, "description": "Recipient addresses"},
418
+ "subject": {"type": "string", "description": "Subject line"},
419
+ "text": {"type": "string", "description": "Plain text body"},
420
+ },
421
+ "required": ["inbox_id", "to", "subject", "text"],
422
+ },
423
+ },
424
+ },
425
+ {
426
+ "type": "function",
427
+ "function": {
428
+ "name": "gork_send_draft",
429
+ "description": "Send an existing email draft.",
430
+ "parameters": {
431
+ "type": "object",
432
+ "properties": {
433
+ "draft_id": {"type": "string", "description": "Draft ID (drf_...)"},
434
+ },
435
+ "required": ["draft_id"],
436
+ },
437
+ },
438
+ },
439
+ ]
440
+
441
+ def execute_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
442
+ """Execute a tool called by an LLM function call."""
443
+ if name == "gork_create_inbox":
444
+ return self.create_inbox(arguments["username"], name=arguments.get("name"))
445
+ elif name == "gork_list_inboxes":
446
+ return self.list_inboxes()
447
+ elif name == "gork_send_email":
448
+ return self.send_message(
449
+ inbox_id=arguments["inbox_id"],
450
+ to=arguments["to"],
451
+ subject=arguments["subject"],
452
+ text=arguments["text"],
453
+ )
454
+ elif name == "gork_read_emails":
455
+ return self.list_messages(inbox_id=arguments.get("inbox_id"), limit=arguments.get("limit", 20))
456
+ elif name == "gork_get_message":
457
+ return self.get_message(arguments["message_id"])
458
+ elif name == "gork_create_draft":
459
+ return self.create_draft(
460
+ inbox_id=arguments["inbox_id"],
461
+ to=arguments["to"],
462
+ subject=arguments["subject"],
463
+ text=arguments["text"],
464
+ )
465
+ elif name == "gork_send_draft":
466
+ return self.send_draft(arguments["draft_id"])
467
+ else:
468
+ raise ValueError(f"Unknown Gork tool: {name}")
@@ -0,0 +1,9 @@
1
+ """Error type carrying the API's stable error code."""
2
+
3
+
4
+ class GorkError(Exception):
5
+ def __init__(self, message: str, code: str = "api_error", status: int = 0, details=None):
6
+ super().__init__(message)
7
+ self.code = code
8
+ self.status = status
9
+ self.details = details
@@ -0,0 +1,59 @@
1
+ """TypedDict shapes mirroring the gork.email API envelopes."""
2
+
3
+ from typing import List, Optional, TypedDict
4
+
5
+
6
+ class Inbox(TypedDict, total=False):
7
+ id: str
8
+ address: str
9
+ name: Optional[str]
10
+ isActive: bool
11
+ createdAt: str
12
+
13
+
14
+ class Message(TypedDict, total=False):
15
+ id: str
16
+ threadId: str
17
+ inboxId: str
18
+ direction: str
19
+ fromAddress: str
20
+ subject: str
21
+ textBody: Optional[str]
22
+ status: str
23
+ createdAt: str
24
+
25
+
26
+ class Thread(TypedDict, total=False):
27
+ id: str
28
+ subject: str
29
+ messageCount: int
30
+ lastMessageAt: str
31
+
32
+
33
+ class Webhook(TypedDict, total=False):
34
+ id: str
35
+ url: str
36
+ subscribedEvents: List[str]
37
+ isActive: bool
38
+ secret: Optional[str]
39
+
40
+
41
+ class ApiKey(TypedDict, total=False):
42
+ id: str
43
+ name: str
44
+ keyPrefix: str
45
+ apiKey: Optional[str]
46
+ scopes: List[str]
47
+ createdAt: str
48
+
49
+
50
+ class Domain(TypedDict, total=False):
51
+ id: str
52
+ domainName: str
53
+ status: str
54
+
55
+
56
+ class Suppression(TypedDict, total=False):
57
+ id: str
58
+ email: str
59
+ reason: str
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: gork-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for gork.email programmable email infrastructure for AI agents
5
+ Author: gork.email
6
+ License: MIT
7
+ Project-URL: Homepage, https://gork.email
8
+ Project-URL: Documentation, https://gork.email/docs
9
+ Project-URL: Repository, https://github.com/shyamcxy/gork-sdk-python
10
+ Project-URL: Issues, https://github.com/shyamcxy/gork-sdk-python/issues
11
+ Keywords: email,agent,ai,mcp,inbox,mail
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Communications :: Email
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+
19
+ # gork-sdk — Python SDK for gork.email
20
+
21
+ > **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
22
+
23
+ Programmable email infrastructure for AI agents. Provision inboxes, send and
24
+ receive mail, search a mailbox, compose drafts, schedule sends, read
25
+ attachments, and manage threads, webhooks, domains, keys, and suppressions.
26
+
27
+ This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
28
+ SDK (`@gork/sdk`); the method names and parameters match.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install gork-sdk
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from gork import GorkClient
40
+
41
+ gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
42
+
43
+ inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
44
+ print(inbox["address"]) # sdr-alex@try.gork.email
45
+
46
+ gork.send_message(
47
+ inbox_id=inbox["id"],
48
+ to=["prospect@acme.com"],
49
+ subject="Quick intro",
50
+ text="Hi — wanted to share what we're building.",
51
+ idempotency_key="outreach-001", # safe retries
52
+ )
53
+ ```
54
+
55
+ ## Webhook verification
56
+
57
+ ```python
58
+ from gork import verify_signature
59
+
60
+ ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
61
+ ```
62
+
63
+ ## Links
64
+
65
+ - Docs: https://gork.email/docs
66
+ - API reference: https://api.gork.email/openapi.json
67
+ - Changelog: https://gork.email/changelog
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/gork/__init__.py
4
+ src/gork/client.py
5
+ src/gork/errors.py
6
+ src/gork/types.py
7
+ src/gork_sdk.egg-info/PKG-INFO
8
+ src/gork_sdk.egg-info/SOURCES.txt
9
+ src/gork_sdk.egg-info/dependency_links.txt
10
+ src/gork_sdk.egg-info/top_level.txt
11
+ tests/test_client_requests.py
@@ -0,0 +1,110 @@
1
+ """Smoke test: the new methods build the right requests (no network)."""
2
+ import json, sys, urllib.request
3
+ sys.path.insert(0, "src")
4
+ from gork import GorkClient
5
+
6
+ calls = []
7
+
8
+ class FakeResponse:
9
+ def __init__(self, payload): self._payload = payload
10
+ def read(self): return json.dumps(self._payload).encode()
11
+ def __enter__(self): return self
12
+ def __exit__(self, *a): return False
13
+
14
+ def fake_urlopen(req, timeout=None):
15
+ calls.append({
16
+ "url": req.full_url,
17
+ "method": req.get_method(),
18
+ "body": json.loads(req.data.decode()) if req.data else None,
19
+ "headers": dict(req.headers),
20
+ })
21
+ if "attachments" in req.full_url and "/v1/attachments/" in req.full_url:
22
+ return FakeResponseBytes(b"FILE-BYTES")
23
+ return FakeResponse({"data": {"id": "msg_1", "status": "delivered"}})
24
+
25
+ class FakeResponseBytes:
26
+ def __init__(self, b): self._b = b
27
+ def read(self): return self._b
28
+ def __enter__(self): return self
29
+ def __exit__(self, *a): return False
30
+
31
+ urllib.request.urlopen = fake_urlopen
32
+ client = GorkClient(api_key="gork_live_test")
33
+
34
+ # attachments + tracking on send
35
+ client.send_message("inb_1", ["a@b.com"], subject="Invoice", text="hi",
36
+ attachments=[{"filename": "i.pdf", "contentType": "application/pdf", "content": "UERG"}],
37
+ track_opens=True)
38
+ body = calls[-1]["body"]
39
+ assert body["attachments"][0]["filename"] == "i.pdf", body
40
+ assert body["trackOpens"] is True
41
+
42
+ # scheduling
43
+ client.send_message("inb_1", ["a@b.com"], subject="Later", send_at="2026-09-12T10:00:00.000Z")
44
+ assert calls[-1]["body"]["sendAt"] == "2026-09-12T10:00:00.000Z"
45
+
46
+ # search filters reach the query string
47
+ client.search_messages("invoice", direction="inbound")
48
+ assert "q=invoice" in calls[-1]["url"], calls[-1]["url"]
49
+ assert "direction=inbound" in calls[-1]["url"]
50
+
51
+ # draft lifecycle
52
+ client.create_draft("inb_1", to=["a@b.com"], subject="Draft")
53
+ assert calls[-1]["method"] == "POST" and calls[-1]["url"].endswith("/v1/drafts")
54
+ client.update_draft("drf_1", subject="Edited")
55
+ assert calls[-1]["method"] == "PATCH"
56
+ client.send_draft("drf_1")
57
+ assert calls[-1]["url"].endswith("/v1/drafts/drf_1/send")
58
+ client.discard_draft("drf_1")
59
+ assert calls[-1]["method"] == "DELETE"
60
+
61
+ # attachment bytes
62
+ data = client.download_attachment("att_1")
63
+ assert data == b"FILE-BYTES", data
64
+
65
+ # cancel a schedule
66
+ client.cancel_scheduled("msg_1")
67
+ assert calls[-1]["method"] == "DELETE" and calls[-1]["url"].endswith("/schedule")
68
+
69
+ # AI agent function tools
70
+ tools = client.get_tools()
71
+ assert len(tools) >= 6
72
+ assert any(t["function"]["name"] == "gork_send_email" for t in tools)
73
+ client.execute_tool("gork_send_email", {
74
+ "inbox_id": "inb_1",
75
+ "to": ["ai@example.com"],
76
+ "subject": "Agent Hello",
77
+ "text": "Sent via tool",
78
+ })
79
+ assert calls[-1]["body"]["to"] == ["ai@example.com"]
80
+
81
+ print("python SDK smoke test passed:", len(calls), "calls checked")
82
+
83
+
84
+ # ── Version drift guard ────────────────────────────────────────────────────
85
+ # The User-Agent header used to hardcode a version string; it now reads the
86
+ # installed distribution metadata, and these keep it honest and pre-alpha.
87
+ def test_version_matches_pyproject():
88
+ import re, pathlib
89
+ root = pathlib.Path(__file__).resolve().parent.parent
90
+ declared = re.search(r'^version = "([^"]+)"', (root / "pyproject.toml").read_text(), re.M).group(1)
91
+ assert declared == "0.1.0", f"expected the 0.x pre-alpha line, got {declared}"
92
+
93
+
94
+ def test_user_agent_uses_the_helper():
95
+ import pathlib
96
+ src = (pathlib.Path(__file__).resolve().parent.parent / "src" / "gork" / "client.py").read_text()
97
+ assert "gork-sdk-python/{_client_version()}" in src
98
+ assert not __import__("re").search(r'gork-sdk-python/\d', src)
99
+
100
+
101
+ def test_reported_version_is_pre_alpha():
102
+ import sys, pathlib
103
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
104
+ from gork.client import _client_version
105
+ assert _client_version().startswith("0.")
106
+
107
+ test_version_matches_pyproject()
108
+ test_user_agent_uses_the_helper()
109
+ test_reported_version_is_pre_alpha()
110
+ print("all version drift guards passed")