mailtea 0.1.2__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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
mailtea-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mailtea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mailtea-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: mailtea
3
+ Version: 0.1.2
4
+ Summary: Mailtea Python SDK — send, schedule, and manage email from your app or AI agent.
5
+ Project-URL: Homepage, https://mailtea.app
6
+ Project-URL: Documentation, https://docs.mailtea.app
7
+ Project-URL: Repository, https://github.com/mailtea-app/mailtea-python
8
+ Author: Mailtea
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agent,email,mailtea,sdk,transactional-email
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Communications :: Email
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+
18
+ # mailtea (Python)
19
+
20
+ The official Python SDK for [Mailtea](https://mailtea.app) — a thin, zero-dependency
21
+ wrapper over the [REST API](https://api.mailtea.app). Python 3.9+.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install mailtea
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ import os
33
+ from mailtea import Mailtea
34
+
35
+ mailtea = Mailtea(os.environ["MAILTEA_API_KEY"])
36
+
37
+ sent = mailtea.emails.send(
38
+ from_="you@yourdomain.com",
39
+ to="recipient@example.com",
40
+ subject="Hello from Mailtea",
41
+ html="<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
42
+ )
43
+ print(sent.id)
44
+
45
+ email = mailtea.emails.get(sent.id)
46
+ print(email.status)
47
+ ```
48
+
49
+ The API key can be passed to `Mailtea(api_key)` or read from `MAILTEA_API_KEY`.
50
+ Self-hosting or local dev? pass `base_url=` or set `MAILTEA_API_BASE_URL`.
51
+
52
+ Every method equally accepts a single wire-format dict — handy when you already
53
+ hold the JSON payload:
54
+
55
+ ```python
56
+ sent = mailtea.emails.send({
57
+ "from": "you@yourdomain.com",
58
+ "to": "recipient@example.com",
59
+ "subject": "Hello from Mailtea",
60
+ "html": "<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
61
+ })
62
+ print(sent["id"]) # responses support ["..."] and attribute access alike
63
+ ```
64
+
65
+ Keyword arguments use snake_case wire names; a trailing underscore escapes
66
+ Python reserved words (`from_=` → `"from"`).
67
+
68
+ ## API
69
+
70
+ | Method | Description |
71
+ | --- | --- |
72
+ | `emails.send(params)` | Send a transactional email → `{"id": ...}` |
73
+ | `emails.batch(emails)` | Send up to 100 emails → `{"data": [{"id": ...}]}` |
74
+ | `emails.get(id)` | Retrieve an email and its delivery status |
75
+ | `emails.list(params=None)` | List emails → `{"data", "total", "limit", "offset", "has_more"}` |
76
+ | `emails.update(id, params)` | Reschedule a scheduled email |
77
+ | `emails.reschedule(id, scheduled_at)` | Convenience wrapper over `update` |
78
+ | `emails.cancel(id)` | Cancel a scheduled email |
79
+ | `contacts.create / upsert / list / get / update / delete` | Manage audience contacts (`upsert` = `create`; the endpoint upserts) |
80
+ | `posts.create(...)` | Create a newsletter post (draft, or `send=True`) → `{"id": ...}` |
81
+ | `posts.send(id, scheduled_at=None)` | Send a draft post to the audience, now or scheduled |
82
+ | `posts.send_test(id, params)` | Send a `[TEST]` copy of a post → `{"sent_to", "failed_to"}` |
83
+ | `segments.create / list / get / update / delete` | Manage audience segments |
84
+ | `tags.create / list / get / update / delete` | Manage tag definitions |
85
+ | `domains.create / list / get / verify / update / delete` | Manage sending domains (add, read DNS records, verify) |
86
+ | `domains.tracking.create / list / verify / delete` | Manage CNAME tracking sub-domains under a domain |
87
+ | `webhooks.create / list / get / update / delete` | Manage outbound event subscriptions |
88
+ | `contact_properties.create / list / update / delete` | Manage custom contact fields (team-scoped) |
89
+ | `api_keys.create / list / revoke` | Manage API keys (`settings:write`) |
90
+
91
+ Payloads follow the REST wire format (`reply_to`, `scheduled_at`, …), passed as
92
+ keyword arguments or a plain dict. Errors raise `MailteaError` with `status`,
93
+ `details`, and `request_id`.
94
+
95
+ ## Develop
96
+
97
+ ```bash
98
+ cd sdks/python
99
+ python3 -m unittest discover -s tests -t .
100
+ ```
@@ -0,0 +1,83 @@
1
+ # mailtea (Python)
2
+
3
+ The official Python SDK for [Mailtea](https://mailtea.app) — a thin, zero-dependency
4
+ wrapper over the [REST API](https://api.mailtea.app). Python 3.9+.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install mailtea
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```python
15
+ import os
16
+ from mailtea import Mailtea
17
+
18
+ mailtea = Mailtea(os.environ["MAILTEA_API_KEY"])
19
+
20
+ sent = mailtea.emails.send(
21
+ from_="you@yourdomain.com",
22
+ to="recipient@example.com",
23
+ subject="Hello from Mailtea",
24
+ html="<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
25
+ )
26
+ print(sent.id)
27
+
28
+ email = mailtea.emails.get(sent.id)
29
+ print(email.status)
30
+ ```
31
+
32
+ The API key can be passed to `Mailtea(api_key)` or read from `MAILTEA_API_KEY`.
33
+ Self-hosting or local dev? pass `base_url=` or set `MAILTEA_API_BASE_URL`.
34
+
35
+ Every method equally accepts a single wire-format dict — handy when you already
36
+ hold the JSON payload:
37
+
38
+ ```python
39
+ sent = mailtea.emails.send({
40
+ "from": "you@yourdomain.com",
41
+ "to": "recipient@example.com",
42
+ "subject": "Hello from Mailtea",
43
+ "html": "<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
44
+ })
45
+ print(sent["id"]) # responses support ["..."] and attribute access alike
46
+ ```
47
+
48
+ Keyword arguments use snake_case wire names; a trailing underscore escapes
49
+ Python reserved words (`from_=` → `"from"`).
50
+
51
+ ## API
52
+
53
+ | Method | Description |
54
+ | --- | --- |
55
+ | `emails.send(params)` | Send a transactional email → `{"id": ...}` |
56
+ | `emails.batch(emails)` | Send up to 100 emails → `{"data": [{"id": ...}]}` |
57
+ | `emails.get(id)` | Retrieve an email and its delivery status |
58
+ | `emails.list(params=None)` | List emails → `{"data", "total", "limit", "offset", "has_more"}` |
59
+ | `emails.update(id, params)` | Reschedule a scheduled email |
60
+ | `emails.reschedule(id, scheduled_at)` | Convenience wrapper over `update` |
61
+ | `emails.cancel(id)` | Cancel a scheduled email |
62
+ | `contacts.create / upsert / list / get / update / delete` | Manage audience contacts (`upsert` = `create`; the endpoint upserts) |
63
+ | `posts.create(...)` | Create a newsletter post (draft, or `send=True`) → `{"id": ...}` |
64
+ | `posts.send(id, scheduled_at=None)` | Send a draft post to the audience, now or scheduled |
65
+ | `posts.send_test(id, params)` | Send a `[TEST]` copy of a post → `{"sent_to", "failed_to"}` |
66
+ | `segments.create / list / get / update / delete` | Manage audience segments |
67
+ | `tags.create / list / get / update / delete` | Manage tag definitions |
68
+ | `domains.create / list / get / verify / update / delete` | Manage sending domains (add, read DNS records, verify) |
69
+ | `domains.tracking.create / list / verify / delete` | Manage CNAME tracking sub-domains under a domain |
70
+ | `webhooks.create / list / get / update / delete` | Manage outbound event subscriptions |
71
+ | `contact_properties.create / list / update / delete` | Manage custom contact fields (team-scoped) |
72
+ | `api_keys.create / list / revoke` | Manage API keys (`settings:write`) |
73
+
74
+ Payloads follow the REST wire format (`reply_to`, `scheduled_at`, …), passed as
75
+ keyword arguments or a plain dict. Errors raise `MailteaError` with `status`,
76
+ `details`, and `request_id`.
77
+
78
+ ## Develop
79
+
80
+ ```bash
81
+ cd sdks/python
82
+ python3 -m unittest discover -s tests -t .
83
+ ```
@@ -0,0 +1,7 @@
1
+ """Mailtea Python SDK — send, schedule, and manage email from your app or AI agent."""
2
+
3
+ from .client import Mailtea
4
+ from .errors import MailteaError
5
+
6
+ __all__ = ["Mailtea", "MailteaError"]
7
+ __version__ = "0.1.2"
@@ -0,0 +1,54 @@
1
+ """Shared helpers for resource classes: payload merging, query strings, and
2
+ attribute-access response wrapping."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, Optional
7
+ from urllib.parse import urlencode
8
+
9
+
10
+ def body(params: Optional[Dict[str, Any]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
11
+ """Merge a positional wire-format dict with keyword arguments.
12
+
13
+ Every method accepts either style (or both — kwargs win on conflict)::
14
+
15
+ mailtea.emails.send({"from": "a@b.c", "subject": "Hi"})
16
+ mailtea.emails.send(from_="a@b.c", subject="Hi")
17
+
18
+ A single trailing underscore is stripped from kwarg names so Python
19
+ reserved words can be used (``from_`` becomes ``"from"`` on the wire).
20
+ """
21
+ merged = dict(params or {})
22
+ for key, value in kwargs.items():
23
+ merged[key[:-1] if key.endswith("_") else key] = value
24
+ return merged
25
+
26
+
27
+ def query(params: Optional[Dict[str, Any]]) -> str:
28
+ """Render a query string from a dict, dropping ``None`` values."""
29
+ if not params:
30
+ return ""
31
+ clean = {key: value for key, value in params.items() if value is not None}
32
+ return ("?" + urlencode(clean)) if clean else ""
33
+
34
+
35
+ class Resource(dict):
36
+ """An API response: a plain dict that also allows attribute access, so
37
+ both ``email["id"]`` and ``email.id`` work. Nested dicts/lists are wrapped
38
+ on access. Keys that collide with dict methods (``get``, ``items``, …)
39
+ are only reachable via ``[...]`` indexing.
40
+ """
41
+
42
+ def __getattr__(self, name: str) -> Any:
43
+ try:
44
+ return wrap(self[name])
45
+ except KeyError:
46
+ raise AttributeError(name) from None
47
+
48
+
49
+ def wrap(value: Any) -> Any:
50
+ if isinstance(value, dict):
51
+ return Resource(value)
52
+ if isinstance(value, list):
53
+ return [wrap(item) for item in value]
54
+ return value
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.error
4
+ import urllib.request
5
+ from typing import Callable, Dict, Iterable, NamedTuple, Optional, Tuple
6
+
7
+
8
+ class HttpResponse(NamedTuple):
9
+ status: int
10
+ headers: Dict[str, str]
11
+ text: str
12
+
13
+
14
+ # (method, url, headers, body) -> HttpResponse. Injectable so tests need no network.
15
+ Transport = Callable[[str, str, Dict[str, str], Optional[bytes]], HttpResponse]
16
+
17
+
18
+ def _lower_headers(items: Iterable[Tuple[str, str]]) -> Dict[str, str]:
19
+ return {key.lower(): value for key, value in items}
20
+
21
+
22
+ def urllib_transport(
23
+ method: str, url: str, headers: Dict[str, str], body: Optional[bytes]
24
+ ) -> HttpResponse:
25
+ """Default transport built on the standard library — no third-party deps."""
26
+ request = urllib.request.Request(url=url, data=body, headers=headers, method=method)
27
+ try:
28
+ with urllib.request.urlopen(request) as response: # noqa: S310 (trusted base URL)
29
+ return HttpResponse(
30
+ response.status,
31
+ _lower_headers(response.headers.items()),
32
+ response.read().decode("utf-8"),
33
+ )
34
+ except urllib.error.HTTPError as error:
35
+ text = error.read().decode("utf-8") if error.fp is not None else ""
36
+ return HttpResponse(error.code, _lower_headers(error.headers.items()), text)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+ from urllib.parse import quote
5
+
6
+ from ._resource import body as _body
7
+
8
+ RequestFn = Callable[..., Any]
9
+
10
+
11
+ class ApiKeys:
12
+ """The ``api_keys`` resource. Access via ``mailtea.api_keys``.
13
+
14
+ Requires a token with ``settings:write``. A key can never be granted scopes
15
+ the calling token does not already hold.
16
+ """
17
+
18
+ def __init__(self, request: RequestFn) -> None:
19
+ self._request = request
20
+
21
+ def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
22
+ """Create an API key. The ``token`` is returned ONCE — store it securely.
23
+
24
+ Takes ``name``, optional ``permission`` (``"full_access"`` or
25
+ ``"sending_access"``), and optional ``domain_id``. Accepts a
26
+ wire-format dict, keyword arguments, or both.
27
+ """
28
+ return self._request("POST", "/v1/api-keys", _body(params, kwargs))
29
+
30
+ def list(self) -> Dict[str, Any]:
31
+ """List API keys (token values are never returned)."""
32
+ return self._request("GET", "/v1/api-keys")
33
+
34
+ def revoke(self, id: str) -> Any:
35
+ """Revoke (delete) an API key by id."""
36
+ return self._request("DELETE", "/v1/api-keys/" + quote(str(id), safe=""))
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, Optional
6
+
7
+ from ._resource import wrap as _wrap
8
+ from ._transport import Transport, urllib_transport
9
+ from .api_keys import ApiKeys
10
+ from .contact_properties import ContactProperties
11
+ from .contacts import Contacts
12
+ from .domains import Domains
13
+ from .emails import Emails
14
+ from .errors import MailteaError
15
+ from .posts import Posts
16
+ from .segments import Segments
17
+ from .tags import Tags
18
+ from .webhooks import Webhooks
19
+
20
+ DEFAULT_BASE_URL = "https://api.mailtea.app"
21
+
22
+
23
+ class Mailtea:
24
+ """The Mailtea client.
25
+
26
+ >>> from mailtea import Mailtea
27
+ >>> mailtea = Mailtea(os.environ["MAILTEA_API_KEY"])
28
+ >>> sent = mailtea.emails.send(
29
+ ... from_="you@yourdomain.com",
30
+ ... to="recipient@example.com",
31
+ ... subject="Hello",
32
+ ... html="<p>Sent with Mailtea.</p>",
33
+ ... )
34
+ >>> sent.id # responses allow attribute and ["..."] access alike
35
+
36
+ Payloads may equally be passed as a single wire-format dict
37
+ (``send({"from": ..., "to": ...})``) — both styles hit the same endpoint.
38
+
39
+ The API key may be passed explicitly or read from ``MAILTEA_API_KEY``.
40
+ Self-hosting/local dev: pass ``base_url`` or set ``MAILTEA_API_BASE_URL``.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: Optional[str] = None,
46
+ *,
47
+ base_url: Optional[str] = None,
48
+ transport: Optional[Transport] = None,
49
+ ) -> None:
50
+ key = api_key or os.environ.get("MAILTEA_API_KEY")
51
+ if not key:
52
+ raise MailteaError(
53
+ "Missing Mailtea API key. Pass it to Mailtea(api_key) or set the "
54
+ "MAILTEA_API_KEY environment variable.",
55
+ code="missing_api_key",
56
+ )
57
+ self._api_key = key
58
+ self._base_url = (
59
+ base_url or os.environ.get("MAILTEA_API_BASE_URL") or DEFAULT_BASE_URL
60
+ ).rstrip("/")
61
+ self._transport: Transport = transport or urllib_transport
62
+
63
+ self.emails = Emails(self._request)
64
+ self.contacts = Contacts(self._request)
65
+ self.posts = Posts(self._request)
66
+ self.segments = Segments(self._request)
67
+ self.tags = Tags(self._request)
68
+ self.domains = Domains(self._request)
69
+ self.webhooks = Webhooks(self._request)
70
+ self.contact_properties = ContactProperties(self._request)
71
+ self.api_keys = ApiKeys(self._request)
72
+
73
+ def _request(self, method: str, path: str, body: Any = None) -> Any:
74
+ url = self._base_url + path
75
+ headers = {"Authorization": "Bearer " + self._api_key}
76
+ data: Optional[bytes] = None
77
+ if body is not None:
78
+ headers["Content-Type"] = "application/json"
79
+ data = json.dumps(body).encode("utf-8")
80
+
81
+ response = self._transport(method, url, headers, data)
82
+ request_id = response.headers.get("x-request-id")
83
+
84
+ if response.status >= 400:
85
+ message = "HTTP {0}".format(response.status)
86
+ details = None
87
+ try:
88
+ parsed = json.loads(response.text)
89
+ if isinstance(parsed, dict):
90
+ message = parsed.get("error") or message
91
+ details = parsed.get("details")
92
+ except ValueError:
93
+ pass # non-JSON body — keep the status-line message
94
+ raise MailteaError(
95
+ message, status=response.status, details=details, request_id=request_id
96
+ )
97
+
98
+ if response.status == 204 or not response.text:
99
+ return None
100
+ # Responses are dicts that also allow attribute access
101
+ # (email["id"] and email.id both work).
102
+ return _wrap(json.loads(response.text))
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+ from urllib.parse import quote
5
+
6
+ from ._resource import body as _body, query as _query
7
+
8
+ RequestFn = Callable[..., Any]
9
+
10
+
11
+ class ContactProperties:
12
+ """The ``contact_properties`` resource (custom contact fields). Access via
13
+ ``mailtea.contact_properties``.
14
+
15
+ Definitions are team-scoped — there is no ``publication_id``. ``create``
16
+ takes ``key`` and ``type`` (``"string"`` or ``"number"``). Every method
17
+ accepts the payload as a wire-format dict, as keyword arguments, or both.
18
+ """
19
+
20
+ def __init__(self, request: RequestFn) -> None:
21
+ self._request = request
22
+
23
+ def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
24
+ return self._request("POST", "/v1/contact-properties", _body(params, kwargs))
25
+
26
+ def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
27
+ return self._request("GET", "/v1/contact-properties" + _query(_body(params, kwargs)))
28
+
29
+ def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
30
+ return self._request(
31
+ "PATCH", "/v1/contact-properties/" + quote(str(id), safe=""), _body(params, kwargs)
32
+ )
33
+
34
+ def delete(self, id: str) -> Dict[str, Any]:
35
+ return self._request(
36
+ "DELETE", "/v1/contact-properties/" + quote(str(id), safe="")
37
+ )
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+ from urllib.parse import quote
5
+
6
+ from ._resource import body as _body, query as _query
7
+
8
+ RequestFn = Callable[..., Any]
9
+
10
+
11
+ class Contacts:
12
+ """The ``contacts`` resource. Access via ``mailtea.contacts``.
13
+
14
+ Audience resources are scoped to a publication — pass ``publication_id``.
15
+ Every method accepts the payload as a wire-format dict, as keyword
16
+ arguments, or both.
17
+ """
18
+
19
+ def __init__(self, request: RequestFn) -> None:
20
+ self._request = request
21
+
22
+ def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
23
+ """Create a contact — or update it if the email already exists in the
24
+ publication (the endpoint upserts). :meth:`upsert` is the same call."""
25
+ return self._request("POST", "/v1/contacts", _body(params, kwargs))
26
+
27
+ def upsert(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
28
+ """Create the contact or update it in place — alias of :meth:`create`,
29
+ named for what ``POST /v1/contacts`` actually does."""
30
+ return self.create(params, **kwargs)
31
+
32
+ def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
33
+ return self._request("GET", "/v1/contacts" + _query(_body(params, kwargs)))
34
+
35
+ def get(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
36
+ return self._request(
37
+ "GET",
38
+ "/v1/contacts/" + quote(str(id_or_email), safe="") + _query(_body(params, kwargs)),
39
+ )
40
+
41
+ def update(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
42
+ merged = _body(params, kwargs)
43
+ return self._request(
44
+ "PATCH",
45
+ "/v1/contacts/"
46
+ + quote(str(id_or_email), safe="")
47
+ + _query({"publication_id": merged.get("publication_id")}),
48
+ merged,
49
+ )
50
+
51
+ def delete(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
52
+ return self._request(
53
+ "DELETE",
54
+ "/v1/contacts/" + quote(str(id_or_email), safe="") + _query(_body(params, kwargs)),
55
+ )
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+ from urllib.parse import quote
5
+
6
+ from ._resource import body as _body, query as _query
7
+
8
+ RequestFn = Callable[..., Any]
9
+
10
+
11
+ class Domains:
12
+ """The ``domains`` resource (email/site sending domains). Access via
13
+ ``mailtea.domains``.
14
+
15
+ Scoped to a publication — pass ``publication_id``. Register a domain, add the
16
+ returned DNS ``records``, then :meth:`verify` it before sending from it.
17
+ Every method accepts the payload as a wire-format dict, as keyword
18
+ arguments, or both.
19
+ """
20
+
21
+ def __init__(self, request: RequestFn) -> None:
22
+ self._request = request
23
+ #: Tracking sub-domains (CNAME) under a domain.
24
+ self.tracking = TrackingDomains(request)
25
+
26
+ def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
27
+ """Register a domain. The response ``records`` lists the DNS records to add."""
28
+ return self._request("POST", "/v1/domains", _body(params, kwargs))
29
+
30
+ def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
31
+ return self._request("GET", "/v1/domains" + _query(_body(params, kwargs)))
32
+
33
+ def get(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
34
+ return self._request(
35
+ "GET", "/v1/domains/" + quote(str(id), safe="") + _query(_body(params, kwargs))
36
+ )
37
+
38
+ def verify(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
39
+ """Verify a domain via its DNS records; ``status`` becomes ``"verified"``."""
40
+ return self._request(
41
+ "POST",
42
+ "/v1/domains/" + quote(str(id), safe="") + "/verify" + _query(_body(params, kwargs)),
43
+ )
44
+
45
+ def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
46
+ merged = _body(params, kwargs)
47
+ return self._request(
48
+ "PATCH",
49
+ "/v1/domains/"
50
+ + quote(str(id), safe="")
51
+ + _query({"publication_id": merged.get("publication_id")}),
52
+ merged,
53
+ )
54
+
55
+ def delete(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
56
+ return self._request(
57
+ "DELETE", "/v1/domains/" + quote(str(id), safe="") + _query(_body(params, kwargs))
58
+ )
59
+
60
+
61
+ class TrackingDomains:
62
+ """Tracking sub-domains (CNAME) under a domain — used to serve open-pixel
63
+ and click-tracking links from your own domain. Access via
64
+ ``mailtea.domains.tracking``.
65
+ """
66
+
67
+ def __init__(self, request: RequestFn) -> None:
68
+ self._request = request
69
+
70
+ def create(self, domain_id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
71
+ """Add a tracking sub-domain. Takes ``publication_id`` and ``subdomain``.
72
+ The response ``records`` lists the CNAME to add."""
73
+ merged = _body(params, kwargs)
74
+ return self._request(
75
+ "POST",
76
+ "/v1/domains/"
77
+ + quote(str(domain_id), safe="")
78
+ + "/tracking-domains"
79
+ + _query({"publication_id": merged.get("publication_id")}),
80
+ {"subdomain": merged.get("subdomain")},
81
+ )
82
+
83
+ def list(self, domain_id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
84
+ return self._request(
85
+ "GET",
86
+ "/v1/domains/"
87
+ + quote(str(domain_id), safe="")
88
+ + "/tracking-domains"
89
+ + _query(_body(params, kwargs)),
90
+ )
91
+
92
+ def verify(
93
+ self,
94
+ domain_id: str,
95
+ tracking_domain_id: str,
96
+ params: Optional[Dict[str, Any]] = None,
97
+ **kwargs: Any,
98
+ ) -> Dict[str, Any]:
99
+ return self._request(
100
+ "POST",
101
+ "/v1/domains/"
102
+ + quote(str(domain_id), safe="")
103
+ + "/tracking-domains/"
104
+ + quote(str(tracking_domain_id), safe="")
105
+ + "/verify"
106
+ + _query(_body(params, kwargs)),
107
+ )
108
+
109
+ def delete(
110
+ self,
111
+ domain_id: str,
112
+ tracking_domain_id: str,
113
+ params: Optional[Dict[str, Any]] = None,
114
+ **kwargs: Any,
115
+ ) -> Dict[str, Any]:
116
+ return self._request(
117
+ "DELETE",
118
+ "/v1/domains/"
119
+ + quote(str(domain_id), safe="")
120
+ + "/tracking-domains/"
121
+ + quote(str(tracking_domain_id), safe="")
122
+ + _query(_body(params, kwargs)),
123
+ )