Skip to content

Add email templates API support #17

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
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="<div>Hi</div>")
)

# 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
1 change: 1 addition & 0 deletions mailtrap/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
66 changes: 66 additions & 0 deletions mailtrap/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,19 +17,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 +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"
Expand All @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions mailtrap/email_template.py
Original file line number Diff line number Diff line change
@@ -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,
}
)
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
137 changes: 137 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading