diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bb32bf..e8e0b58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [2.2.0] - 2025-05-20
+Add Email Templates API support in MailtrapClient
+
## [2.1.0] - 2025-05-12
- Add sandbox mode support in MailtrapClient
- It requires inbox_id parameter to be set
diff --git a/README.md b/README.md
index c27c5d0..ed8592d 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,47 @@ client = mt.MailtrapClient(token="your-api-key")
client.send(mail)
```
+### Managing templates
+
+Mailtrap provides a dedicated API to manage reusable email templates. The
+client exposes several helper methods which all expect your account identifier
+as the first argument:
+
+- `email_templates(account_id)` – list all templates
+- `create_email_template(account_id, data)` – create a new template
+- `update_email_template(account_id, template_id, data)` – update a template
+- `delete_email_template(account_id, template_id)` – remove a template
+
+#### Creating a template
+
+The `data` dictionary **must** contain `name`, `subject` and `category`. You can
+optionally provide `body_html` and/or `body_text` to store the template content.
+
+```python
+import mailtrap as mt
+
+client = mt.MailtrapClient(token="your-api-key")
+
+# list templates
+templates = client.email_templates(1)
+
+template_data = {
+ "name": "Welcome",
+ "subject": "Welcome on board",
+ "category": "Promotion",
+ "body_html": "
Hello {{user_name}}
",
+ "body_text": "Hello {{user_name}}",
+}
+
+created = client.create_email_template(1, template_data)
+
+# update template
+client.update_email_template(1, created["id"], {"subject": "New subject"})
+
+# delete template
+client.delete_email_template(1, created["id"])
+```
+
## Contributing
Bug reports and pull requests are welcome on [GitHub](https://github.com/railsware/mailtrap-python). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
diff --git a/mailtrap/client.py b/mailtrap/client.py
index 60a1d6a..2bf2be4 100644
--- a/mailtrap/client.py
+++ b/mailtrap/client.py
@@ -1,3 +1,4 @@
+from typing import Any
from typing import NoReturn
from typing import Optional
from typing import Union
@@ -15,12 +16,14 @@ class MailtrapClient:
DEFAULT_PORT = 443
BULK_HOST = "bulk.api.mailtrap.io"
SANDBOX_HOST = "sandbox.api.mailtrap.io"
+ TEMPLATES_HOST = "mailtrap.io"
def __init__(
self,
token: str,
api_host: Optional[str] = None,
api_port: int = DEFAULT_PORT,
+ app_host: Optional[str] = None,
bulk: bool = False,
sandbox: bool = False,
inbox_id: Optional[str] = None,
@@ -28,6 +31,7 @@ def __init__(
self.token = token
self.api_host = api_host
self.api_port = api_port
+ self.app_host = app_host
self.bulk = bulk
self.sandbox = sandbox
self.inbox_id = inbox_id
@@ -45,10 +49,60 @@ def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
self._handle_failed_response(response)
+ def email_templates(self, account_id: int) -> list[dict[str, Any]]:
+ response = requests.get(self._templates_url(account_id), headers=self.headers)
+
+ if response.ok:
+ data: list[dict[str, Any]] = response.json()
+ return data
+
+ self._handle_failed_response(response)
+
+ def create_email_template(
+ self, account_id: int, data: dict[str, Any]
+ ) -> dict[str, Any]:
+ response = requests.post(
+ self._templates_url(account_id), headers=self.headers, json=data
+ )
+
+ if response.status_code == 201:
+ return response.json()
+
+ self._handle_failed_response(response)
+
+ def update_email_template(
+ self, account_id: int, template_id: int, data: dict[str, Any]
+ ) -> dict[str, Any]:
+ response = requests.patch(
+ self._templates_url(account_id, template_id),
+ headers=self.headers,
+ json=data,
+ )
+
+ if response.ok:
+ return response.json()
+
+ self._handle_failed_response(response)
+
+ def delete_email_template(self, account_id: int, template_id: int) -> None:
+ response = requests.delete(
+ self._templates_url(account_id, template_id), headers=self.headers
+ )
+
+ if response.status_code == 204:
+ return None
+
+ self._handle_failed_response(response)
+
@property
def base_url(self) -> str:
return f"https://{self._host.rstrip('/')}:{self.api_port}"
+ @property
+ def app_base_url(self) -> str:
+ host = self.app_host if self.app_host else self.TEMPLATES_HOST
+ return f"https://{host.rstrip('/')}"
+
@property
def api_send_url(self) -> str:
url = f"{self.base_url}/api/send"
@@ -67,6 +121,12 @@ def headers(self) -> dict[str, str]:
),
}
+ def _templates_url(self, account_id: int, template_id: Optional[int] = None) -> str:
+ url = f"{self.app_base_url}/api/accounts/{account_id}/email_templates"
+ if template_id is not None:
+ url = f"{url}/{template_id}"
+ return url
+
@property
def _host(self) -> str:
if self.api_host:
diff --git a/pyproject.toml b/pyproject.toml
index 73834a3..d399299 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "mailtrap"
-version = "2.1.0"
+version = "2.2.0"
description = "Official mailtrap.io API client"
readme = "README.md"
license = {file = "LICENSE.txt"}
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
index 29d3679..d3224b1 100644
--- a/tests/unit/test_client.py
+++ b/tests/unit/test_client.py
@@ -142,3 +142,68 @@ def test_send_should_raise_api_error_for_500_status_code(
with pytest.raises(mt.APIError):
client.send(mail)
+
+ TEMPLATES_URL = "https://mailtrap.io/api/accounts/1/email_templates"
+ TEMPLATE_DETAIL_URL = "https://mailtrap.io/api/accounts/1/email_templates/5"
+
+ @responses.activate
+ def test_email_templates_should_return_list(self) -> None:
+ response_body = [{"id": 1}, {"id": 2}]
+ responses.add(responses.GET, self.TEMPLATES_URL, json=response_body)
+
+ client = self.get_client()
+ result = client.email_templates(1)
+
+ assert result == response_body
+ assert len(responses.calls) == 1
+ request = responses.calls[0].request # type: ignore
+ assert request.headers.items() >= client.headers.items()
+
+ @responses.activate
+ def test_create_email_template_should_return_created_template(self) -> None:
+ request_body = {"name": "Template"}
+ response_body = {"id": 5}
+ responses.add(
+ responses.POST,
+ self.TEMPLATES_URL,
+ json=response_body,
+ status=201,
+ )
+
+ client = self.get_client()
+ result = client.create_email_template(1, request_body)
+
+ assert result == response_body
+ request = responses.calls[0].request # type: ignore
+ assert request.body == json.dumps(request_body).encode()
+
+ @responses.activate
+ def test_update_email_template_should_return_updated_template(self) -> None:
+ request_body = {"name": "Template"}
+ response_body = {"id": 5, "name": "Template"}
+ responses.add(
+ responses.PATCH,
+ self.TEMPLATE_DETAIL_URL,
+ json=response_body,
+ )
+
+ client = self.get_client()
+ result = client.update_email_template(1, 5, request_body)
+
+ assert result == response_body
+ request = responses.calls[0].request # type: ignore
+ assert request.body == json.dumps(request_body).encode()
+
+ @responses.activate
+ def test_delete_email_template_should_return_none(self) -> None:
+ responses.add(
+ responses.DELETE,
+ self.TEMPLATE_DETAIL_URL,
+ status=204,
+ )
+
+ client = self.get_client()
+ result = client.delete_email_template(1, 5)
+
+ assert result is None
+ assert len(responses.calls) == 1