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..e5de474 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,44 @@ client = mt.MailtrapClient(token="your-api-key") client.send(mail) ``` +### Managing templates + +You can manage templates stored in your Mailtrap account using `MailtrapClient`. +When creating a template the following fields are required: + +- `name` +- `subject` +- `category` + +Optional fields are `body_html` and `body_text`. + +```python +import mailtrap as mt + +client = mt.MailtrapClient(token="your-api-key") + +# list templates +templates = client.email_templates(account_id=1) + +# create template +new_template = mt.EmailTemplate( + name="Welcome", + subject="subject", + category="Promotion", +) +created = client.create_email_template(1, new_template) + +# update template +updated = client.update_email_template( + 1, + created["id"], + mt.EmailTemplate(name="Welcome", subject="subject", category="Promotion", body_html="
Hi
") +) + +# 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/__init__.py b/mailtrap/__init__.py index f03b693..98732ee 100644 --- a/mailtrap/__init__.py +++ b/mailtrap/__init__.py @@ -1,4 +1,5 @@ from .client import MailtrapClient +from .email_template import EmailTemplate from .exceptions import APIError from .exceptions import AuthorizationError from .exceptions import ClientConfigurationError diff --git a/mailtrap/client.py b/mailtrap/client.py index 60a1d6a..6e89fc1 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -1,9 +1,11 @@ +from typing import Any from typing import NoReturn from typing import Optional from typing import Union import requests +from mailtrap.email_template import EmailTemplate from mailtrap.exceptions import APIError from mailtrap.exceptions import AuthorizationError from mailtrap.exceptions import ClientConfigurationError @@ -15,12 +17,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 +32,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 +50,65 @@ 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, template: Union[EmailTemplate, dict[str, Any]] + ) -> dict[str, Any]: + json_data = template.api_data if isinstance(template, EmailTemplate) else template + response = requests.post( + self._templates_url(account_id), headers=self.headers, json=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, + template: Union[EmailTemplate, dict[str, Any]], + ) -> dict[str, Any]: + json_data = template.api_data if isinstance(template, EmailTemplate) else template + response = requests.patch( + self._templates_url(account_id, template_id), + headers=self.headers, + json=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 +127,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/mailtrap/email_template.py b/mailtrap/email_template.py new file mode 100644 index 0000000..031c798 --- /dev/null +++ b/mailtrap/email_template.py @@ -0,0 +1,32 @@ +from typing import Any +from typing import Optional + +from mailtrap.mail.base_entity import BaseEntity + + +class EmailTemplate(BaseEntity): + def __init__( + self, + name: str, + subject: str, + category: str, + body_html: Optional[str] = None, + body_text: Optional[str] = None, + ) -> None: + self.name = name + self.subject = subject + self.category = category + self.body_html = body_html + self.body_text = body_text + + @property + def api_data(self) -> dict[str, Any]: + return self.omit_none_values( + { + "name": self.name, + "subject": self.subject, + "category": self.category, + "body_html": self.body_html, + "body_text": self.body_text, + } + ) 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..fb2da10 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -142,3 +142,140 @@ 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_email_templates_should_raise_error(self) -> None: + responses.add( + responses.GET, + self.TEMPLATES_URL, + json={"errors": ["Unauthorized"]}, + status=401, + ) + + client = self.get_client() + + with pytest.raises(mt.AuthorizationError): + client.email_templates(1) + + @responses.activate + def test_email_templates_should_raise_api_error(self) -> None: + responses.add( + responses.GET, + self.TEMPLATES_URL, + json={"errors": ["fail"]}, + status=500, + ) + + client = self.get_client() + + with pytest.raises(mt.APIError): + client.email_templates(1) + + @responses.activate + def test_create_email_template_should_return_created_template(self) -> None: + template = mt.EmailTemplate(name="Template", subject="s", category="Cat") + 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, template) + + assert result == response_body + request = responses.calls[0].request # type: ignore + assert request.body == json.dumps(template.api_data).encode() + + @responses.activate + def test_create_email_template_should_raise_error(self) -> None: + template = mt.EmailTemplate(name="Template", subject="s", category="Cat") + responses.add( + responses.POST, + self.TEMPLATES_URL, + json={"errors": ["fail"]}, + status=500, + ) + + client = self.get_client() + + with pytest.raises(mt.APIError): + client.create_email_template(1, template) + + @responses.activate + def test_update_email_template_should_return_updated_template(self) -> None: + template = mt.EmailTemplate(name="Template", subject="s", category="Cat") + 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, template) + + assert result == response_body + request = responses.calls[0].request # type: ignore + assert request.body == json.dumps(template.api_data).encode() + + @responses.activate + def test_update_email_template_should_raise_error(self) -> None: + template = mt.EmailTemplate(name="Template", subject="s", category="Cat") + responses.add( + responses.PATCH, + self.TEMPLATE_DETAIL_URL, + json={"errors": ["fail"]}, + status=401, + ) + + client = self.get_client() + + with pytest.raises(mt.AuthorizationError): + client.update_email_template(1, 5, template) + + @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 + + @responses.activate + def test_delete_email_template_should_raise_error(self) -> None: + responses.add( + responses.DELETE, + self.TEMPLATE_DETAIL_URL, + json={"errors": ["fail"]}, + status=500, + ) + + client = self.get_client() + + with pytest.raises(mt.APIError): + client.delete_email_template(1, 5)