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