Skip to content

Add email templates API support #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<h1>Hello {{user_name}}</h1>",
"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).
Expand Down
60 changes: 60 additions & 0 deletions mailtrap/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Any
from typing import NoReturn
from typing import Optional
from typing import Union
Expand All @@ -15,19 +16,22 @@ 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,
) -> None:
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
Expand All @@ -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"
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading