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)