mailwire-sdk 0.1.1__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.
- mailwire_sdk-0.1.1/.gitignore +28 -0
- mailwire_sdk-0.1.1/PKG-INFO +8 -0
- mailwire_sdk-0.1.1/README.md +120 -0
- mailwire_sdk-0.1.1/pyproject.toml +21 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/__init__.py +33 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/_transport.py +69 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/client.py +76 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/errors.py +90 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/mcp_server.py +278 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/py.typed +0 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/__init__.py +19 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/api_keys.py +55 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/domains.py +44 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/inboxes.py +81 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/messages.py +78 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/tenants.py +48 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/threads.py +35 -0
- mailwire_sdk-0.1.1/src/mailwire_sdk/resources/webhooks.py +85 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
scripts/.last_thread_id
|
|
20
|
+
/inbound/
|
|
21
|
+
|
|
22
|
+
/var/
|
|
23
|
+
infra/.bootstrap_admin_key.txt
|
|
24
|
+
|
|
25
|
+
.DS_Store
|
|
26
|
+
|
|
27
|
+
.claude/
|
|
28
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# mailwire-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for [mailwire-x1](https://github.com/anthropics/mailwire-x1).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mailwire-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
To also install the MCP server entry point:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install 'mailwire-sdk[mcp]'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from mailwire_sdk import Client
|
|
21
|
+
|
|
22
|
+
with Client(token="mw_live_...", base_url="https://api.example.com") as mw:
|
|
23
|
+
# Create an inbox on a verified tenant domain
|
|
24
|
+
inbox = mw.inboxes.create(
|
|
25
|
+
client_id="lena.taiwo",
|
|
26
|
+
display_name="Lena",
|
|
27
|
+
domain_id="dom_...",
|
|
28
|
+
shareable_local_part="lena",
|
|
29
|
+
private_address_count=2,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Send a message
|
|
33
|
+
msg = mw.messages.send(
|
|
34
|
+
inbox_id=inbox["id"],
|
|
35
|
+
from_address="Lena <lena@someonehq.com>",
|
|
36
|
+
to=["recipient@example.com"],
|
|
37
|
+
subject="Catching up",
|
|
38
|
+
text="Hi — thinking of you.",
|
|
39
|
+
idempotency_key="20260503-lena-001",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# List recent messages
|
|
43
|
+
for m in mw.messages.list(inbox_id=inbox["id"], limit=20):
|
|
44
|
+
print(m["id"], m["subject"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Sub-clients
|
|
48
|
+
|
|
49
|
+
| Attribute | Resource |
|
|
50
|
+
|----------------|-------------------------------------------------------|
|
|
51
|
+
| `mw.tenants` | tenant CRUD (`admin` scope) |
|
|
52
|
+
| `mw.api_keys` | mint / list / get / revoke (`admin` scope) |
|
|
53
|
+
| `mw.domains` | register, fetch DNS records, verify, delete |
|
|
54
|
+
| `mw.inboxes` | create, list, get, update, archive, add_address |
|
|
55
|
+
| `mw.messages` | send, list, get |
|
|
56
|
+
|
|
57
|
+
### Errors
|
|
58
|
+
|
|
59
|
+
Every non-2xx response from mailwire becomes a typed exception:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from mailwire_sdk import (
|
|
63
|
+
Client,
|
|
64
|
+
AuthError,
|
|
65
|
+
PermissionError,
|
|
66
|
+
NotFoundError,
|
|
67
|
+
ConflictError,
|
|
68
|
+
ValidationError,
|
|
69
|
+
ProviderError,
|
|
70
|
+
MailwireError, # base — catch-all
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
mw.messages.send(...)
|
|
75
|
+
except ConflictError as exc:
|
|
76
|
+
# idempotency-key reused with a different body
|
|
77
|
+
print(exc.code, exc.details)
|
|
78
|
+
except PermissionError as exc:
|
|
79
|
+
# token is missing the required scope
|
|
80
|
+
print("missing:", exc.details["required_scope"])
|
|
81
|
+
except MailwireError as exc:
|
|
82
|
+
# any other API error — exc.type, exc.status_code, exc.request_id available
|
|
83
|
+
raise
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## MCP server
|
|
87
|
+
|
|
88
|
+
`mailwire-sdk[mcp]` ships a stdio MCP server. Wire it into Claude Desktop / your
|
|
89
|
+
IDE:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"mailwire": {
|
|
95
|
+
"command": "mailwire-mcp",
|
|
96
|
+
"env": {
|
|
97
|
+
"MAILWIRE_BASE_URL": "https://api.example.com",
|
|
98
|
+
"MAILWIRE_TOKEN": "mw_live_..."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Tools exposed:
|
|
106
|
+
|
|
107
|
+
- `mailwire_list_inboxes`
|
|
108
|
+
- `mailwire_get_inbox`
|
|
109
|
+
- `mailwire_send_message`
|
|
110
|
+
- `mailwire_list_messages`
|
|
111
|
+
- `mailwire_get_message`
|
|
112
|
+
|
|
113
|
+
See [`docs/mcp.md`](../../docs/mcp.md) for tool argument schemas and the
|
|
114
|
+
end-to-end agent loop.
|
|
115
|
+
|
|
116
|
+
## Versioning
|
|
117
|
+
|
|
118
|
+
`mailwire-sdk 0.1.0` targets the mailwire-x1 v1 API. Breaking server changes go
|
|
119
|
+
to a new `/v2/` surface and a major-version SDK bump. Additive changes (new
|
|
120
|
+
optional fields, new endpoints) ship in patch / minor versions.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mailwire-sdk"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python SDK for mailwire-x1"
|
|
5
|
+
requires-python = ">=3.12,<3.13"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"httpx>=0.27,<0.28",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[project.optional-dependencies]
|
|
11
|
+
mcp = ["mcp>=1.0"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
mailwire-mcp = "mailwire_sdk.mcp_server:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/mailwire_sdk"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Python SDK for mailwire-x1.
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
- `Client` — synchronous client (use as context manager).
|
|
5
|
+
- `MailwireError` and subclasses for catching API errors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mailwire_sdk.client import Client
|
|
9
|
+
from mailwire_sdk.errors import (
|
|
10
|
+
AuthError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
MailwireError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
PermissionError,
|
|
15
|
+
ProviderError,
|
|
16
|
+
ServerError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AuthError",
|
|
24
|
+
"Client",
|
|
25
|
+
"ConflictError",
|
|
26
|
+
"MailwireError",
|
|
27
|
+
"NotFoundError",
|
|
28
|
+
"PermissionError",
|
|
29
|
+
"ProviderError",
|
|
30
|
+
"ServerError",
|
|
31
|
+
"ValidationError",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""HTTP transport — wraps httpx.Client with auth + error envelope decoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from mailwire_sdk.errors import MailwireError, from_envelope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Transport:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
token: str,
|
|
17
|
+
base_url: str,
|
|
18
|
+
timeout: float = 30.0,
|
|
19
|
+
user_agent: str = "mailwire-sdk-python/0.1.0",
|
|
20
|
+
) -> None:
|
|
21
|
+
self._client = httpx.Client(
|
|
22
|
+
base_url=base_url.rstrip("/"),
|
|
23
|
+
headers={
|
|
24
|
+
"Authorization": f"Bearer {token}",
|
|
25
|
+
"User-Agent": user_agent,
|
|
26
|
+
"Accept": "application/json",
|
|
27
|
+
},
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def request(
|
|
32
|
+
self,
|
|
33
|
+
method: str,
|
|
34
|
+
path: str,
|
|
35
|
+
*,
|
|
36
|
+
params: dict[str, Any] | None = None,
|
|
37
|
+
json: dict[str, Any] | None = None,
|
|
38
|
+
headers: dict[str, str] | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
try:
|
|
41
|
+
response = self._client.request(
|
|
42
|
+
method, path, params=params, json=json, headers=headers
|
|
43
|
+
)
|
|
44
|
+
except httpx.RequestError as exc:
|
|
45
|
+
raise MailwireError(f"network error: {exc}") from exc
|
|
46
|
+
|
|
47
|
+
if response.status_code == 204 or not response.content:
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
payload = response.json()
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise MailwireError(
|
|
54
|
+
f"non-JSON response (HTTP {response.status_code}): {response.text[:200]!r}"
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
if response.status_code >= 400:
|
|
58
|
+
raise from_envelope(payload, status_code=response.status_code)
|
|
59
|
+
|
|
60
|
+
return payload # type: ignore[no-any-return]
|
|
61
|
+
|
|
62
|
+
def close(self) -> None:
|
|
63
|
+
self._client.close()
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> Transport:
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, *exc: object) -> None:
|
|
69
|
+
self.close()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Top-level mailwire SDK client.
|
|
2
|
+
|
|
3
|
+
The synchronous `Client` is the v0 surface — async support and generated typed
|
|
4
|
+
models land in Sprint 5. Usage:
|
|
5
|
+
|
|
6
|
+
from mailwire_sdk import Client
|
|
7
|
+
with Client(token="mw_live_...", base_url="https://api.example.com") as mw:
|
|
8
|
+
inbox = mw.inboxes.create(
|
|
9
|
+
client_id="lena.taiwo",
|
|
10
|
+
display_name="Lena",
|
|
11
|
+
domain_id="dom_...",
|
|
12
|
+
shareable_local_part="lena",
|
|
13
|
+
private_address_count=2,
|
|
14
|
+
)
|
|
15
|
+
mw.messages.send(
|
|
16
|
+
inbox_id=inbox["id"],
|
|
17
|
+
from_address="Lena <lena@someonehq.com>",
|
|
18
|
+
to=["recipient@example.com"],
|
|
19
|
+
subject="Catching up",
|
|
20
|
+
text="...",
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from mailwire_sdk._transport import Transport
|
|
29
|
+
from mailwire_sdk.resources import (
|
|
30
|
+
ApiKeysResource,
|
|
31
|
+
DomainsResource,
|
|
32
|
+
InboxesResource,
|
|
33
|
+
MessagesResource,
|
|
34
|
+
TenantsResource,
|
|
35
|
+
ThreadsResource,
|
|
36
|
+
WebhooksResource,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Client:
|
|
41
|
+
"""Synchronous mailwire client.
|
|
42
|
+
|
|
43
|
+
Use as a context manager (recommended) or call `.close()` when done. Each
|
|
44
|
+
`Client` owns a pooled `httpx.Client` under the hood.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
token: str,
|
|
51
|
+
base_url: str = "http://localhost:8083",
|
|
52
|
+
timeout: float = 30.0,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._transport = Transport(token=token, base_url=base_url, timeout=timeout)
|
|
55
|
+
self.tenants = TenantsResource(self._transport)
|
|
56
|
+
self.api_keys = ApiKeysResource(self._transport)
|
|
57
|
+
self.domains = DomainsResource(self._transport)
|
|
58
|
+
self.inboxes = InboxesResource(self._transport)
|
|
59
|
+
self.messages = MessagesResource(self._transport)
|
|
60
|
+
self.threads = ThreadsResource(self._transport)
|
|
61
|
+
self.webhooks = WebhooksResource(self._transport)
|
|
62
|
+
|
|
63
|
+
def healthz(self) -> dict[str, Any]:
|
|
64
|
+
"""Service health probe — no auth required server-side, but the SDK
|
|
65
|
+
still sends the bearer header. Returns `{"status": "ok", "version": ...}`.
|
|
66
|
+
"""
|
|
67
|
+
return self._transport.request("GET", "/healthz")
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
self._transport.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> Client:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, *exc: object) -> None:
|
|
76
|
+
self.close()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""SDK error hierarchy mirrors the API error envelope.
|
|
2
|
+
|
|
3
|
+
Server response:
|
|
4
|
+
|
|
5
|
+
{ "error": { "type": "validation_error", "code": "...", "message": "...",
|
|
6
|
+
"request_id": "...", "details": {...} } }
|
|
7
|
+
|
|
8
|
+
is parsed and raised as a typed subclass of `MailwireError`. Callers can either
|
|
9
|
+
catch the broad `MailwireError` or be specific (`AuthError`, `NotFoundError`,
|
|
10
|
+
`ConflictError`, etc.).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MailwireError(Exception):
|
|
19
|
+
"""Base class for every error raised by the SDK."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
*,
|
|
25
|
+
type: str | None = None,
|
|
26
|
+
code: str | None = None,
|
|
27
|
+
status_code: int | None = None,
|
|
28
|
+
details: dict[str, Any] | None = None,
|
|
29
|
+
request_id: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.type = type
|
|
33
|
+
self.code = code
|
|
34
|
+
self.status_code = status_code
|
|
35
|
+
self.details: dict[str, Any] = details or {}
|
|
36
|
+
self.request_id = request_id
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthError(MailwireError):
|
|
40
|
+
"""401 — missing or invalid bearer token."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PermissionError(MailwireError):
|
|
44
|
+
"""403 — token lacks required scope."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotFoundError(MailwireError):
|
|
48
|
+
"""404 — resource missing or owned by another tenant."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ConflictError(MailwireError):
|
|
52
|
+
"""409 — uniqueness conflict or idempotency-key reuse with a different body."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ValidationError(MailwireError):
|
|
56
|
+
"""422 / 400 — request body failed validation."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProviderError(MailwireError):
|
|
60
|
+
"""502/503-ish — upstream provider failed."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ServerError(MailwireError):
|
|
64
|
+
"""5xx — internal error."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_BY_TYPE: dict[str, type[MailwireError]] = {
|
|
68
|
+
"auth_error": AuthError,
|
|
69
|
+
"permission_error": PermissionError,
|
|
70
|
+
"not_found": NotFoundError,
|
|
71
|
+
"conflict": ConflictError,
|
|
72
|
+
"validation_error": ValidationError,
|
|
73
|
+
"request_error": ValidationError,
|
|
74
|
+
"provider_error": ProviderError,
|
|
75
|
+
"server_error": ServerError,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def from_envelope(envelope: dict[str, Any], *, status_code: int) -> MailwireError:
|
|
80
|
+
"""Construct the right subclass from an `{"error": {...}}` payload."""
|
|
81
|
+
err = envelope.get("error", envelope)
|
|
82
|
+
cls = _BY_TYPE.get(err.get("type", ""), MailwireError)
|
|
83
|
+
return cls(
|
|
84
|
+
err.get("message", "request failed"),
|
|
85
|
+
type=err.get("type"),
|
|
86
|
+
code=err.get("code"),
|
|
87
|
+
status_code=status_code,
|
|
88
|
+
details=err.get("details"),
|
|
89
|
+
request_id=err.get("request_id"),
|
|
90
|
+
)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""mailwire MCP server (stdio transport).
|
|
2
|
+
|
|
3
|
+
Exposes the inbox + message read/write surface as MCP tools so an AI agent host
|
|
4
|
+
(Claude Desktop, IDE extensions, etc.) can drive mailwire on a user's behalf.
|
|
5
|
+
|
|
6
|
+
Run via the `mailwire-mcp` console script:
|
|
7
|
+
|
|
8
|
+
MAILWIRE_BASE_URL=https://api.example.com \\
|
|
9
|
+
MAILWIRE_TOKEN=mw_live_... \\
|
|
10
|
+
mailwire-mcp
|
|
11
|
+
|
|
12
|
+
Or wire it into an MCP host config:
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"mailwire": {
|
|
17
|
+
"command": "mailwire-mcp",
|
|
18
|
+
"env": {
|
|
19
|
+
"MAILWIRE_BASE_URL": "https://api.example.com",
|
|
20
|
+
"MAILWIRE_TOKEN": "mw_live_..."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
The server is intentionally thin — every tool is a direct passthrough to the
|
|
27
|
+
SDK with the response JSON-encoded back to the caller. No business logic lives
|
|
28
|
+
here.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
import contextlib
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from mcp.server import Server
|
|
41
|
+
from mcp.server.stdio import stdio_server
|
|
42
|
+
from mcp.types import TextContent, Tool
|
|
43
|
+
|
|
44
|
+
from mailwire_sdk import Client, MailwireError
|
|
45
|
+
|
|
46
|
+
_TOOLS: list[dict[str, Any]] = [
|
|
47
|
+
{
|
|
48
|
+
"name": "mailwire_list_inboxes",
|
|
49
|
+
"description": (
|
|
50
|
+
"List inboxes for the authenticated tenant. Optionally filter by "
|
|
51
|
+
"workspace_id or project_id. Returns a JSON array of inbox objects "
|
|
52
|
+
"with their addresses inlined."
|
|
53
|
+
),
|
|
54
|
+
"inputSchema": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"workspace_id": {"type": "string"},
|
|
58
|
+
"project_id": {"type": "string"},
|
|
59
|
+
"limit": {"type": "integer", "default": 50, "maximum": 200},
|
|
60
|
+
},
|
|
61
|
+
"additionalProperties": False,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "mailwire_get_inbox",
|
|
66
|
+
"description": "Fetch a single inbox by its public ID (inb_*).",
|
|
67
|
+
"inputSchema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {"inbox_id": {"type": "string"}},
|
|
70
|
+
"required": ["inbox_id"],
|
|
71
|
+
"additionalProperties": False,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "mailwire_send_message",
|
|
76
|
+
"description": (
|
|
77
|
+
"Send an outbound email. `from_address` may be bare "
|
|
78
|
+
"(`name@domain`) or display-name form (`Name <name@domain>`). "
|
|
79
|
+
"Provide `text` or `html` (or both). Pass `idempotency_key` if "
|
|
80
|
+
"you want safe retries."
|
|
81
|
+
),
|
|
82
|
+
"inputSchema": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"inbox_id": {"type": "string"},
|
|
86
|
+
"from_address": {"type": "string"},
|
|
87
|
+
"to": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
88
|
+
"cc": {"type": "array", "items": {"type": "string"}},
|
|
89
|
+
"bcc": {"type": "array", "items": {"type": "string"}},
|
|
90
|
+
"subject": {"type": "string"},
|
|
91
|
+
"text": {"type": "string"},
|
|
92
|
+
"html": {"type": "string"},
|
|
93
|
+
"sending_domain_id": {"type": "string"},
|
|
94
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
95
|
+
"metadata": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
96
|
+
"idempotency_key": {"type": "string"},
|
|
97
|
+
},
|
|
98
|
+
"required": ["inbox_id", "from_address", "to"],
|
|
99
|
+
"additionalProperties": False,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "mailwire_list_messages",
|
|
104
|
+
"description": (
|
|
105
|
+
"List recent messages for the tenant. Filter by inbox_id, "
|
|
106
|
+
"thread_id, or direction (`outbound` / `inbound`)."
|
|
107
|
+
),
|
|
108
|
+
"inputSchema": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"inbox_id": {"type": "string"},
|
|
112
|
+
"thread_id": {"type": "string"},
|
|
113
|
+
"direction": {"type": "string", "enum": ["inbound", "outbound"]},
|
|
114
|
+
"limit": {"type": "integer", "default": 50, "maximum": 200},
|
|
115
|
+
},
|
|
116
|
+
"additionalProperties": False,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "mailwire_get_message",
|
|
121
|
+
"description": "Fetch a single message by its public ID (msg_*).",
|
|
122
|
+
"inputSchema": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {"message_id": {"type": "string"}},
|
|
125
|
+
"required": ["message_id"],
|
|
126
|
+
"additionalProperties": False,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "mailwire_list_webhooks",
|
|
131
|
+
"description": (
|
|
132
|
+
"List active webhook subscribers for the tenant. Each entry "
|
|
133
|
+
"carries the URL, events_subscribed, secret_prefix, and "
|
|
134
|
+
"last delivery status."
|
|
135
|
+
),
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {},
|
|
139
|
+
"additionalProperties": False,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "mailwire_register_webhook",
|
|
144
|
+
"description": (
|
|
145
|
+
"Register a new webhook subscriber. Returns the plaintext signing "
|
|
146
|
+
"secret EXACTLY ONCE — record it on the subscriber side."
|
|
147
|
+
),
|
|
148
|
+
"inputSchema": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"name": {"type": "string"},
|
|
152
|
+
"url": {"type": "string"},
|
|
153
|
+
"events_subscribed": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"items": {"type": "string"},
|
|
156
|
+
"minItems": 1,
|
|
157
|
+
},
|
|
158
|
+
"environment": {"type": "string", "enum": ["live", "test"]},
|
|
159
|
+
"description": {"type": "string"},
|
|
160
|
+
},
|
|
161
|
+
"required": ["name", "url", "events_subscribed"],
|
|
162
|
+
"additionalProperties": False,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "mailwire_get_webhook_deliveries",
|
|
167
|
+
"description": (
|
|
168
|
+
"Fetch the most recent delivery attempts for a webhook subscriber "
|
|
169
|
+
"(useful for debugging why an event didn't arrive)."
|
|
170
|
+
),
|
|
171
|
+
"inputSchema": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"properties": {"endpoint_id": {"type": "string"}},
|
|
174
|
+
"required": ["endpoint_id"],
|
|
175
|
+
"additionalProperties": False,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _dispatch(client: Client, name: str, args: dict[str, Any]) -> Any: # noqa: PLR0911 — pure dispatch table
|
|
182
|
+
"""Call the right SDK method for `name` and return the raw response."""
|
|
183
|
+
if name == "mailwire_list_inboxes":
|
|
184
|
+
return client.inboxes.list(
|
|
185
|
+
workspace_id=args.get("workspace_id"),
|
|
186
|
+
project_id=args.get("project_id"),
|
|
187
|
+
limit=args.get("limit", 50),
|
|
188
|
+
)
|
|
189
|
+
if name == "mailwire_get_inbox":
|
|
190
|
+
return client.inboxes.get(args["inbox_id"])
|
|
191
|
+
if name == "mailwire_send_message":
|
|
192
|
+
return client.messages.send(
|
|
193
|
+
inbox_id=args["inbox_id"],
|
|
194
|
+
from_address=args["from_address"],
|
|
195
|
+
to=args["to"],
|
|
196
|
+
cc=args.get("cc"),
|
|
197
|
+
bcc=args.get("bcc"),
|
|
198
|
+
subject=args.get("subject", ""),
|
|
199
|
+
text=args.get("text"),
|
|
200
|
+
html=args.get("html"),
|
|
201
|
+
sending_domain_id=args.get("sending_domain_id"),
|
|
202
|
+
tags=args.get("tags"),
|
|
203
|
+
metadata=args.get("metadata"),
|
|
204
|
+
idempotency_key=args.get("idempotency_key"),
|
|
205
|
+
)
|
|
206
|
+
if name == "mailwire_list_messages":
|
|
207
|
+
return client.messages.list(
|
|
208
|
+
inbox_id=args.get("inbox_id"),
|
|
209
|
+
thread_id=args.get("thread_id"),
|
|
210
|
+
direction=args.get("direction"),
|
|
211
|
+
limit=args.get("limit", 50),
|
|
212
|
+
)
|
|
213
|
+
if name == "mailwire_get_message":
|
|
214
|
+
return client.messages.get(args["message_id"])
|
|
215
|
+
if name == "mailwire_list_webhooks":
|
|
216
|
+
return client.webhooks.list()
|
|
217
|
+
if name == "mailwire_register_webhook":
|
|
218
|
+
return client.webhooks.create(
|
|
219
|
+
name=args["name"],
|
|
220
|
+
url=args["url"],
|
|
221
|
+
events_subscribed=args["events_subscribed"],
|
|
222
|
+
environment=args.get("environment", "live"),
|
|
223
|
+
description=args.get("description"),
|
|
224
|
+
)
|
|
225
|
+
if name == "mailwire_get_webhook_deliveries":
|
|
226
|
+
return client.webhooks.deliveries(args["endpoint_id"])
|
|
227
|
+
raise ValueError(f"unknown tool: {name}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def _run() -> None:
|
|
231
|
+
base_url = os.environ.get("MAILWIRE_BASE_URL", "http://localhost:8083")
|
|
232
|
+
token = os.environ.get("MAILWIRE_TOKEN")
|
|
233
|
+
if not token:
|
|
234
|
+
print(
|
|
235
|
+
"MAILWIRE_TOKEN env var is required to run the mailwire MCP server",
|
|
236
|
+
file=sys.stderr,
|
|
237
|
+
)
|
|
238
|
+
raise SystemExit(2)
|
|
239
|
+
|
|
240
|
+
client = Client(token=token, base_url=base_url)
|
|
241
|
+
server: Server = Server("mailwire")
|
|
242
|
+
|
|
243
|
+
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
|
|
244
|
+
async def _list() -> list[Tool]:
|
|
245
|
+
return [Tool(**spec) for spec in _TOOLS]
|
|
246
|
+
|
|
247
|
+
@server.call_tool() # type: ignore[untyped-decorator]
|
|
248
|
+
async def _call(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
249
|
+
try:
|
|
250
|
+
result = await asyncio.to_thread(_dispatch, client, name, arguments or {})
|
|
251
|
+
except MailwireError as exc:
|
|
252
|
+
payload = {
|
|
253
|
+
"ok": False,
|
|
254
|
+
"error": {
|
|
255
|
+
"type": exc.type,
|
|
256
|
+
"code": exc.code,
|
|
257
|
+
"message": str(exc),
|
|
258
|
+
"details": exc.details,
|
|
259
|
+
"request_id": exc.request_id,
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
return [TextContent(type="text", text=json.dumps(payload, indent=2))]
|
|
263
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]
|
|
264
|
+
|
|
265
|
+
async with stdio_server() as (read, write):
|
|
266
|
+
await server.run(read, write, server.create_initialization_options())
|
|
267
|
+
|
|
268
|
+
client.close()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main() -> None:
|
|
272
|
+
"""Console entry point — `mailwire-mcp`."""
|
|
273
|
+
with contextlib.suppress(KeyboardInterrupt):
|
|
274
|
+
asyncio.run(_run())
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Per-resource SDK clients."""
|
|
2
|
+
|
|
3
|
+
from mailwire_sdk.resources.api_keys import ApiKeysResource
|
|
4
|
+
from mailwire_sdk.resources.domains import DomainsResource
|
|
5
|
+
from mailwire_sdk.resources.inboxes import InboxesResource
|
|
6
|
+
from mailwire_sdk.resources.messages import MessagesResource
|
|
7
|
+
from mailwire_sdk.resources.tenants import TenantsResource
|
|
8
|
+
from mailwire_sdk.resources.threads import ThreadsResource
|
|
9
|
+
from mailwire_sdk.resources.webhooks import WebhooksResource
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ApiKeysResource",
|
|
13
|
+
"DomainsResource",
|
|
14
|
+
"InboxesResource",
|
|
15
|
+
"MessagesResource",
|
|
16
|
+
"TenantsResource",
|
|
17
|
+
"ThreadsResource",
|
|
18
|
+
"WebhooksResource",
|
|
19
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""API-key CRUD under `/v1/tenants/{t}/api_keys` — admin-scoped.
|
|
2
|
+
|
|
3
|
+
The mint response includes a `token` field exactly once. Surface it to the user
|
|
4
|
+
immediately; the server keeps only an Argon2id hash.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mailwire_sdk._transport import Transport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiKeysResource:
|
|
16
|
+
def __init__(self, transport: Transport) -> None:
|
|
17
|
+
self._t = transport
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
tenant_id: str,
|
|
22
|
+
*,
|
|
23
|
+
name: str,
|
|
24
|
+
scopes: list[str],
|
|
25
|
+
environment: str = "live",
|
|
26
|
+
workspace_id: str | None = None,
|
|
27
|
+
project_id: str | None = None,
|
|
28
|
+
inbox_id: str | None = None,
|
|
29
|
+
expires_at: datetime | None = None,
|
|
30
|
+
metadata: dict[str, Any] | None = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
body: dict[str, Any] = {
|
|
33
|
+
"name": name,
|
|
34
|
+
"scopes": scopes,
|
|
35
|
+
"environment": environment,
|
|
36
|
+
"metadata": metadata or {},
|
|
37
|
+
}
|
|
38
|
+
if workspace_id is not None:
|
|
39
|
+
body["workspace_id"] = workspace_id
|
|
40
|
+
if project_id is not None:
|
|
41
|
+
body["project_id"] = project_id
|
|
42
|
+
if inbox_id is not None:
|
|
43
|
+
body["inbox_id"] = inbox_id
|
|
44
|
+
if expires_at is not None:
|
|
45
|
+
body["expires_at"] = expires_at.isoformat()
|
|
46
|
+
return self._t.request("POST", f"/v1/tenants/{tenant_id}/api_keys", json=body)
|
|
47
|
+
|
|
48
|
+
def list(self, tenant_id: str) -> list[dict[str, Any]]:
|
|
49
|
+
return self._t.request("GET", f"/v1/tenants/{tenant_id}/api_keys")["data"] # type: ignore[no-any-return]
|
|
50
|
+
|
|
51
|
+
def get(self, tenant_id: str, key_id: str) -> dict[str, Any]:
|
|
52
|
+
return self._t.request("GET", f"/v1/tenants/{tenant_id}/api_keys/{key_id}")
|
|
53
|
+
|
|
54
|
+
def revoke(self, tenant_id: str, key_id: str) -> None:
|
|
55
|
+
self._t.request("DELETE", f"/v1/tenants/{tenant_id}/api_keys/{key_id}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Domain registration + verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mailwire_sdk._transport import Transport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DomainsResource:
|
|
11
|
+
def __init__(self, transport: Transport) -> None:
|
|
12
|
+
self._t = transport
|
|
13
|
+
|
|
14
|
+
def register(
|
|
15
|
+
self,
|
|
16
|
+
tenant_id: str,
|
|
17
|
+
*,
|
|
18
|
+
domain_name: str,
|
|
19
|
+
purpose: str,
|
|
20
|
+
region: str = "us",
|
|
21
|
+
metadata: dict[str, Any] | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
body = {
|
|
24
|
+
"domain_name": domain_name,
|
|
25
|
+
"purpose": purpose,
|
|
26
|
+
"region": region,
|
|
27
|
+
"metadata": metadata or {},
|
|
28
|
+
}
|
|
29
|
+
return self._t.request("POST", f"/v1/tenants/{tenant_id}/domains", json=body)
|
|
30
|
+
|
|
31
|
+
def list_for_tenant(self, tenant_id: str) -> list[dict[str, Any]]:
|
|
32
|
+
return self._t.request("GET", f"/v1/tenants/{tenant_id}/domains")["data"] # type: ignore[no-any-return]
|
|
33
|
+
|
|
34
|
+
def get(self, domain_id: str) -> dict[str, Any]:
|
|
35
|
+
return self._t.request("GET", f"/v1/domains/{domain_id}")
|
|
36
|
+
|
|
37
|
+
def get_dns_records(self, domain_id: str) -> dict[str, Any]:
|
|
38
|
+
return self._t.request("GET", f"/v1/domains/{domain_id}/dns_records")
|
|
39
|
+
|
|
40
|
+
def verify(self, domain_id: str) -> dict[str, Any]:
|
|
41
|
+
return self._t.request("POST", f"/v1/domains/{domain_id}/verify")
|
|
42
|
+
|
|
43
|
+
def delete(self, domain_id: str) -> None:
|
|
44
|
+
self._t.request("DELETE", f"/v1/domains/{domain_id}")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Inbox + address management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mailwire_sdk._transport import Transport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InboxesResource:
|
|
11
|
+
def __init__(self, transport: Transport) -> None:
|
|
12
|
+
self._t = transport
|
|
13
|
+
|
|
14
|
+
def create(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
client_id: str,
|
|
18
|
+
display_name: str,
|
|
19
|
+
domain_id: str,
|
|
20
|
+
shareable_local_part: str | None = None,
|
|
21
|
+
private_address_count: int = 0,
|
|
22
|
+
workspace_id: str | None = None,
|
|
23
|
+
project_id: str | None = None,
|
|
24
|
+
signature_text: str | None = None,
|
|
25
|
+
signature_html: str | None = None,
|
|
26
|
+
timezone: str = "UTC",
|
|
27
|
+
language: str = "en",
|
|
28
|
+
avatar_url: str | None = None,
|
|
29
|
+
metadata: dict[str, Any] | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
body: dict[str, Any] = {
|
|
32
|
+
"client_id": client_id,
|
|
33
|
+
"display_name": display_name,
|
|
34
|
+
"domain_id": domain_id,
|
|
35
|
+
"private_address_count": private_address_count,
|
|
36
|
+
"timezone": timezone,
|
|
37
|
+
"language": language,
|
|
38
|
+
"metadata": metadata or {},
|
|
39
|
+
}
|
|
40
|
+
if shareable_local_part is not None:
|
|
41
|
+
body["shareable_address"] = {"local_part": shareable_local_part}
|
|
42
|
+
for k, v in (
|
|
43
|
+
("workspace_id", workspace_id),
|
|
44
|
+
("project_id", project_id),
|
|
45
|
+
("signature_text", signature_text),
|
|
46
|
+
("signature_html", signature_html),
|
|
47
|
+
("avatar_url", avatar_url),
|
|
48
|
+
):
|
|
49
|
+
if v is not None:
|
|
50
|
+
body[k] = v
|
|
51
|
+
return self._t.request("POST", "/v1/inboxes", json=body)
|
|
52
|
+
|
|
53
|
+
def list(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
workspace_id: str | None = None,
|
|
57
|
+
project_id: str | None = None,
|
|
58
|
+
limit: int = 50,
|
|
59
|
+
) -> list[dict[str, Any]]:
|
|
60
|
+
params: dict[str, Any] = {"limit": limit}
|
|
61
|
+
if workspace_id is not None:
|
|
62
|
+
params["workspace_id"] = workspace_id
|
|
63
|
+
if project_id is not None:
|
|
64
|
+
params["project_id"] = project_id
|
|
65
|
+
return self._t.request("GET", "/v1/inboxes", params=params)["data"] # type: ignore[no-any-return]
|
|
66
|
+
|
|
67
|
+
def get(self, inbox_id: str) -> dict[str, Any]:
|
|
68
|
+
return self._t.request("GET", f"/v1/inboxes/{inbox_id}")
|
|
69
|
+
|
|
70
|
+
def update(
|
|
71
|
+
self,
|
|
72
|
+
inbox_id: str,
|
|
73
|
+
**fields: Any,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
return self._t.request("PATCH", f"/v1/inboxes/{inbox_id}", json=fields)
|
|
76
|
+
|
|
77
|
+
def archive(self, inbox_id: str) -> None:
|
|
78
|
+
self._t.request("DELETE", f"/v1/inboxes/{inbox_id}")
|
|
79
|
+
|
|
80
|
+
def add_address(self, inbox_id: str) -> dict[str, Any]:
|
|
81
|
+
return self._t.request("POST", f"/v1/inboxes/{inbox_id}/addresses")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Outbound + read endpoints for messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mailwire_sdk._transport import Transport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MessagesResource:
|
|
11
|
+
def __init__(self, transport: Transport) -> None:
|
|
12
|
+
self._t = transport
|
|
13
|
+
|
|
14
|
+
def send(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
inbox_id: str,
|
|
18
|
+
from_address: str,
|
|
19
|
+
to: list[str],
|
|
20
|
+
subject: str = "",
|
|
21
|
+
text: str | None = None,
|
|
22
|
+
html: str | None = None,
|
|
23
|
+
cc: list[str] | None = None,
|
|
24
|
+
bcc: list[str] | None = None,
|
|
25
|
+
sending_domain_id: str | None = None,
|
|
26
|
+
in_reply_to_message_id: str | None = None,
|
|
27
|
+
headers: dict[str, str] | None = None,
|
|
28
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
29
|
+
tags: list[str] | None = None,
|
|
30
|
+
tracking_opens: bool = False,
|
|
31
|
+
tracking_clicks: bool = False,
|
|
32
|
+
metadata: dict[str, str] | None = None,
|
|
33
|
+
idempotency_key: str | None = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
body: dict[str, Any] = {
|
|
36
|
+
"inbox_id": inbox_id,
|
|
37
|
+
"from_address": from_address,
|
|
38
|
+
"to": to,
|
|
39
|
+
"cc": cc or [],
|
|
40
|
+
"bcc": bcc or [],
|
|
41
|
+
"subject": subject,
|
|
42
|
+
"headers": headers or {},
|
|
43
|
+
"attachments": attachments or [],
|
|
44
|
+
"tags": tags or [],
|
|
45
|
+
"tracking": {"opens": tracking_opens, "clicks": tracking_clicks},
|
|
46
|
+
"metadata": metadata or {},
|
|
47
|
+
}
|
|
48
|
+
if sending_domain_id is not None:
|
|
49
|
+
body["sending_domain_id"] = sending_domain_id
|
|
50
|
+
if in_reply_to_message_id is not None:
|
|
51
|
+
body["in_reply_to_message_id"] = in_reply_to_message_id
|
|
52
|
+
if text is not None:
|
|
53
|
+
body["text"] = text
|
|
54
|
+
if html is not None:
|
|
55
|
+
body["html"] = html
|
|
56
|
+
request_headers = (
|
|
57
|
+
{"Idempotency-Key": idempotency_key} if idempotency_key is not None else None
|
|
58
|
+
)
|
|
59
|
+
return self._t.request(
|
|
60
|
+
"POST", "/v1/messages", json=body, headers=request_headers
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def list(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
inbox_id: str | None = None,
|
|
67
|
+
thread_id: str | None = None,
|
|
68
|
+
direction: str | None = None,
|
|
69
|
+
limit: int = 50,
|
|
70
|
+
) -> list[dict[str, Any]]:
|
|
71
|
+
params: dict[str, Any] = {"limit": limit}
|
|
72
|
+
for k, v in (("inbox_id", inbox_id), ("thread_id", thread_id), ("direction", direction)):
|
|
73
|
+
if v is not None:
|
|
74
|
+
params[k] = v
|
|
75
|
+
return self._t.request("GET", "/v1/messages", params=params)["data"] # type: ignore[no-any-return]
|
|
76
|
+
|
|
77
|
+
def get(self, message_id: str) -> dict[str, Any]:
|
|
78
|
+
return self._t.request("GET", f"/v1/messages/{message_id}")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tenant CRUD — admin-scoped."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mailwire_sdk._transport import Transport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TenantsResource:
|
|
11
|
+
def __init__(self, transport: Transport) -> None:
|
|
12
|
+
self._t = transport
|
|
13
|
+
|
|
14
|
+
def create(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
name: str,
|
|
18
|
+
slug: str | None = None,
|
|
19
|
+
metadata: dict[str, Any] | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
body: dict[str, Any] = {"name": name, "metadata": metadata or {}}
|
|
22
|
+
if slug is not None:
|
|
23
|
+
body["slug"] = slug
|
|
24
|
+
return self._t.request("POST", "/v1/tenants", json=body)
|
|
25
|
+
|
|
26
|
+
def list(self, *, limit: int = 50) -> list[dict[str, Any]]:
|
|
27
|
+
return self._t.request("GET", "/v1/tenants", params={"limit": limit})["data"] # type: ignore[no-any-return]
|
|
28
|
+
|
|
29
|
+
def get(self, tenant_id: str) -> dict[str, Any]:
|
|
30
|
+
return self._t.request("GET", f"/v1/tenants/{tenant_id}")
|
|
31
|
+
|
|
32
|
+
def update(
|
|
33
|
+
self,
|
|
34
|
+
tenant_id: str,
|
|
35
|
+
*,
|
|
36
|
+
name: str | None = None,
|
|
37
|
+
status: str | None = None,
|
|
38
|
+
metadata: dict[str, Any] | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
body = {
|
|
41
|
+
k: v
|
|
42
|
+
for k, v in (("name", name), ("status", status), ("metadata", metadata))
|
|
43
|
+
if v is not None
|
|
44
|
+
}
|
|
45
|
+
return self._t.request("PATCH", f"/v1/tenants/{tenant_id}", json=body)
|
|
46
|
+
|
|
47
|
+
def delete(self, tenant_id: str) -> None:
|
|
48
|
+
self._t.request("DELETE", f"/v1/tenants/{tenant_id}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Thread list / detail — `/v1/threads`. Gmail-style inbox view."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mailwire_sdk._transport import Transport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThreadsResource:
|
|
11
|
+
def __init__(self, transport: Transport) -> None:
|
|
12
|
+
self._t = transport
|
|
13
|
+
|
|
14
|
+
def get(
|
|
15
|
+
self, thread_id: str, *, include_last_message: bool = True
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
params = {"include_last_message": "true" if include_last_message else "false"}
|
|
18
|
+
return self._t.request("GET", f"/v1/threads/{thread_id}", params=params)
|
|
19
|
+
|
|
20
|
+
# `list` defined LAST so other annotations bind to the builtin
|
|
21
|
+
# (Python class scope is built top-down).
|
|
22
|
+
def list(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
inbox_id: str,
|
|
26
|
+
limit: int = 50,
|
|
27
|
+
include_last_message: bool = True,
|
|
28
|
+
) -> list[dict[str, Any]]:
|
|
29
|
+
params: dict[str, Any] = {
|
|
30
|
+
"inbox_id": inbox_id,
|
|
31
|
+
"limit": limit,
|
|
32
|
+
"include_last_message": "true" if include_last_message else "false",
|
|
33
|
+
}
|
|
34
|
+
data = self._t.request("GET", "/v1/threads", params=params).get("data", [])
|
|
35
|
+
return list(data)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Webhook subscriber CRUD — `/v1/webhooks`.
|
|
2
|
+
|
|
3
|
+
The `secret` field is returned EXACTLY ONCE on `create` and `rotate_secret`.
|
|
4
|
+
Listings + reads expose only `secret_prefix`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from mailwire_sdk._transport import Transport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebhooksResource:
|
|
15
|
+
def __init__(self, transport: Transport) -> None:
|
|
16
|
+
self._t = transport
|
|
17
|
+
|
|
18
|
+
def create(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
name: str,
|
|
22
|
+
url: str,
|
|
23
|
+
events_subscribed: list[str],
|
|
24
|
+
environment: str = "live",
|
|
25
|
+
workspace_id: str | None = None,
|
|
26
|
+
description: str | None = None,
|
|
27
|
+
metadata: dict[str, Any] | None = None,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
body: dict[str, Any] = {
|
|
30
|
+
"name": name,
|
|
31
|
+
"url": url,
|
|
32
|
+
"events_subscribed": events_subscribed,
|
|
33
|
+
"environment": environment,
|
|
34
|
+
"metadata": metadata or {},
|
|
35
|
+
}
|
|
36
|
+
if workspace_id is not None:
|
|
37
|
+
body["workspace_id"] = workspace_id
|
|
38
|
+
if description is not None:
|
|
39
|
+
body["description"] = description
|
|
40
|
+
return self._t.request("POST", "/v1/webhooks", json=body)
|
|
41
|
+
|
|
42
|
+
def get(self, endpoint_id: str) -> dict[str, Any]:
|
|
43
|
+
return self._t.request("GET", f"/v1/webhooks/{endpoint_id}")
|
|
44
|
+
|
|
45
|
+
def update(
|
|
46
|
+
self,
|
|
47
|
+
endpoint_id: str,
|
|
48
|
+
*,
|
|
49
|
+
url: str | None = None,
|
|
50
|
+
events_subscribed: list[str] | None = None,
|
|
51
|
+
enabled: bool | None = None,
|
|
52
|
+
description: str | None = None,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
body: dict[str, Any] = {}
|
|
55
|
+
if url is not None:
|
|
56
|
+
body["url"] = url
|
|
57
|
+
if events_subscribed is not None:
|
|
58
|
+
body["events_subscribed"] = events_subscribed
|
|
59
|
+
if enabled is not None:
|
|
60
|
+
body["enabled"] = enabled
|
|
61
|
+
if description is not None:
|
|
62
|
+
body["description"] = description
|
|
63
|
+
return self._t.request("PATCH", f"/v1/webhooks/{endpoint_id}", json=body)
|
|
64
|
+
|
|
65
|
+
def revoke(self, endpoint_id: str) -> None:
|
|
66
|
+
self._t.request("DELETE", f"/v1/webhooks/{endpoint_id}")
|
|
67
|
+
|
|
68
|
+
def rotate_secret(self, endpoint_id: str) -> dict[str, Any]:
|
|
69
|
+
return self._t.request("POST", f"/v1/webhooks/{endpoint_id}/rotate_secret")
|
|
70
|
+
|
|
71
|
+
def deliveries(
|
|
72
|
+
self, endpoint_id: str, *, limit: int = 50
|
|
73
|
+
) -> list[dict[str, Any]]:
|
|
74
|
+
data = self._t.request(
|
|
75
|
+
"GET",
|
|
76
|
+
f"/v1/webhooks/{endpoint_id}/deliveries",
|
|
77
|
+
params={"limit": limit},
|
|
78
|
+
).get("data", [])
|
|
79
|
+
return list(data)
|
|
80
|
+
|
|
81
|
+
# `list` is defined LAST so other methods' `list[...]` annotations bind
|
|
82
|
+
# to the builtin (Python class scope is built top-down).
|
|
83
|
+
def list(self) -> list[dict[str, Any]]:
|
|
84
|
+
data = self._t.request("GET", "/v1/webhooks").get("data", [])
|
|
85
|
+
return list(data)
|