Skip to content

Commit c2fed51

Browse files
committed
Add EmailTemplate class and extend template API
1 parent 709f096 commit c2fed51

File tree

7 files changed

+278
-1
lines changed

7 files changed

+278
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## [2.2.0] - 2025-05-20
2+
Add Email Templates API support in MailtrapClient
3+
14
## [2.1.0] - 2025-05-12
25
- Add sandbox mode support in MailtrapClient
36
- It requires inbox_id parameter to be set

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,44 @@ client = mt.MailtrapClient(token="your-api-key")
122122
client.send(mail)
123123
```
124124

125+
### Managing templates
126+
127+
You can manage templates stored in your Mailtrap account using `MailtrapClient`.
128+
When creating a template the following fields are required:
129+
130+
- `name`
131+
- `subject`
132+
- `category`
133+
134+
Optional fields are `body_html` and `body_text`.
135+
136+
```python
137+
import mailtrap as mt
138+
139+
client = mt.MailtrapClient(token="your-api-key")
140+
141+
# list templates
142+
templates = client.email_templates(account_id=1)
143+
144+
# create template
145+
new_template = mt.EmailTemplate(
146+
name="Welcome",
147+
subject="subject",
148+
category="Promotion",
149+
)
150+
created = client.create_email_template(1, new_template)
151+
152+
# update template
153+
updated = client.update_email_template(
154+
1,
155+
created["id"],
156+
mt.EmailTemplate(name="Welcome", subject="subject", category="Promotion", body_html="<div>Hi</div>")
157+
)
158+
159+
# delete template
160+
client.delete_email_template(1, created["id"])
161+
```
162+
125163
## Contributing
126164

127165
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).

mailtrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .client import MailtrapClient
2+
from .email_template import EmailTemplate
23
from .exceptions import APIError
34
from .exceptions import AuthorizationError
45
from .exceptions import ClientConfigurationError

mailtrap/client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from typing import Any
12
from typing import NoReturn
23
from typing import Optional
34
from typing import Union
45

56
import requests
67

8+
from mailtrap.email_template import EmailTemplate
79
from mailtrap.exceptions import APIError
810
from mailtrap.exceptions import AuthorizationError
911
from mailtrap.exceptions import ClientConfigurationError
@@ -15,19 +17,22 @@ class MailtrapClient:
1517
DEFAULT_PORT = 443
1618
BULK_HOST = "bulk.api.mailtrap.io"
1719
SANDBOX_HOST = "sandbox.api.mailtrap.io"
20+
TEMPLATES_HOST = "mailtrap.io"
1821

1922
def __init__(
2023
self,
2124
token: str,
2225
api_host: Optional[str] = None,
2326
api_port: int = DEFAULT_PORT,
27+
app_host: Optional[str] = None,
2428
bulk: bool = False,
2529
sandbox: bool = False,
2630
inbox_id: Optional[str] = None,
2731
) -> None:
2832
self.token = token
2933
self.api_host = api_host
3034
self.api_port = api_port
35+
self.app_host = app_host
3136
self.bulk = bulk
3237
self.sandbox = sandbox
3338
self.inbox_id = inbox_id
@@ -45,10 +50,65 @@ def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
4550

4651
self._handle_failed_response(response)
4752

53+
def email_templates(self, account_id: int) -> list[dict[str, Any]]:
54+
response = requests.get(self._templates_url(account_id), headers=self.headers)
55+
56+
if response.ok:
57+
data: list[dict[str, Any]] = response.json()
58+
return data
59+
60+
self._handle_failed_response(response)
61+
62+
def create_email_template(
63+
self, account_id: int, template: Union[EmailTemplate, dict[str, Any]]
64+
) -> dict[str, Any]:
65+
json_data = template.api_data if isinstance(template, EmailTemplate) else template
66+
response = requests.post(
67+
self._templates_url(account_id), headers=self.headers, json=json_data
68+
)
69+
70+
if response.status_code == 201:
71+
return response.json()
72+
73+
self._handle_failed_response(response)
74+
75+
def update_email_template(
76+
self,
77+
account_id: int,
78+
template_id: int,
79+
template: Union[EmailTemplate, dict[str, Any]],
80+
) -> dict[str, Any]:
81+
json_data = template.api_data if isinstance(template, EmailTemplate) else template
82+
response = requests.patch(
83+
self._templates_url(account_id, template_id),
84+
headers=self.headers,
85+
json=json_data,
86+
)
87+
88+
if response.ok:
89+
return response.json()
90+
91+
self._handle_failed_response(response)
92+
93+
def delete_email_template(self, account_id: int, template_id: int) -> None:
94+
response = requests.delete(
95+
self._templates_url(account_id, template_id), headers=self.headers
96+
)
97+
98+
if response.status_code == 204:
99+
return None
100+
101+
self._handle_failed_response(response)
102+
48103
@property
49104
def base_url(self) -> str:
50105
return f"https://{self._host.rstrip('/')}:{self.api_port}"
51106

107+
@property
108+
def app_base_url(self) -> str:
109+
host = self.app_host if self.app_host else self.TEMPLATES_HOST
110+
return f"https://{host.rstrip('/')}"
111+
52112
@property
53113
def api_send_url(self) -> str:
54114
url = f"{self.base_url}/api/send"
@@ -67,6 +127,12 @@ def headers(self) -> dict[str, str]:
67127
),
68128
}
69129

130+
def _templates_url(self, account_id: int, template_id: Optional[int] = None) -> str:
131+
url = f"{self.app_base_url}/api/accounts/{account_id}/email_templates"
132+
if template_id is not None:
133+
url = f"{url}/{template_id}"
134+
return url
135+
70136
@property
71137
def _host(self) -> str:
72138
if self.api_host:

mailtrap/email_template.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Any
2+
from typing import Optional
3+
4+
from mailtrap.mail.base_entity import BaseEntity
5+
6+
7+
class EmailTemplate(BaseEntity):
8+
def __init__(
9+
self,
10+
name: str,
11+
subject: str,
12+
category: str,
13+
body_html: Optional[str] = None,
14+
body_text: Optional[str] = None,
15+
) -> None:
16+
self.name = name
17+
self.subject = subject
18+
self.category = category
19+
self.body_html = body_html
20+
self.body_text = body_text
21+
22+
@property
23+
def api_data(self) -> dict[str, Any]:
24+
return self.omit_none_values(
25+
{
26+
"name": self.name,
27+
"subject": self.subject,
28+
"category": self.category,
29+
"body_html": self.body_html,
30+
"body_text": self.body_text,
31+
}
32+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mailtrap"
3-
version = "2.1.0"
3+
version = "2.2.0"
44
description = "Official mailtrap.io API client"
55
readme = "README.md"
66
license = {file = "LICENSE.txt"}

tests/unit/test_client.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,140 @@ def test_send_should_raise_api_error_for_500_status_code(
142142

143143
with pytest.raises(mt.APIError):
144144
client.send(mail)
145+
146+
TEMPLATES_URL = "https://mailtrap.io/api/accounts/1/email_templates"
147+
TEMPLATE_DETAIL_URL = "https://mailtrap.io/api/accounts/1/email_templates/5"
148+
149+
@responses.activate
150+
def test_email_templates_should_return_list(self) -> None:
151+
response_body = [{"id": 1}, {"id": 2}]
152+
responses.add(responses.GET, self.TEMPLATES_URL, json=response_body)
153+
154+
client = self.get_client()
155+
result = client.email_templates(1)
156+
157+
assert result == response_body
158+
assert len(responses.calls) == 1
159+
request = responses.calls[0].request # type: ignore
160+
assert request.headers.items() >= client.headers.items()
161+
162+
@responses.activate
163+
def test_email_templates_should_raise_error(self) -> None:
164+
responses.add(
165+
responses.GET,
166+
self.TEMPLATES_URL,
167+
json={"errors": ["Unauthorized"]},
168+
status=401,
169+
)
170+
171+
client = self.get_client()
172+
173+
with pytest.raises(mt.AuthorizationError):
174+
client.email_templates(1)
175+
176+
@responses.activate
177+
def test_email_templates_should_raise_api_error(self) -> None:
178+
responses.add(
179+
responses.GET,
180+
self.TEMPLATES_URL,
181+
json={"errors": ["fail"]},
182+
status=500,
183+
)
184+
185+
client = self.get_client()
186+
187+
with pytest.raises(mt.APIError):
188+
client.email_templates(1)
189+
190+
@responses.activate
191+
def test_create_email_template_should_return_created_template(self) -> None:
192+
template = mt.EmailTemplate(name="Template", subject="s", category="Cat")
193+
response_body = {"id": 5}
194+
responses.add(
195+
responses.POST,
196+
self.TEMPLATES_URL,
197+
json=response_body,
198+
status=201,
199+
)
200+
201+
client = self.get_client()
202+
result = client.create_email_template(1, template)
203+
204+
assert result == response_body
205+
request = responses.calls[0].request # type: ignore
206+
assert request.body == json.dumps(template.api_data).encode()
207+
208+
@responses.activate
209+
def test_create_email_template_should_raise_error(self) -> None:
210+
template = mt.EmailTemplate(name="Template", subject="s", category="Cat")
211+
responses.add(
212+
responses.POST,
213+
self.TEMPLATES_URL,
214+
json={"errors": ["fail"]},
215+
status=500,
216+
)
217+
218+
client = self.get_client()
219+
220+
with pytest.raises(mt.APIError):
221+
client.create_email_template(1, template)
222+
223+
@responses.activate
224+
def test_update_email_template_should_return_updated_template(self) -> None:
225+
template = mt.EmailTemplate(name="Template", subject="s", category="Cat")
226+
response_body = {"id": 5, "name": "Template"}
227+
responses.add(
228+
responses.PATCH,
229+
self.TEMPLATE_DETAIL_URL,
230+
json=response_body,
231+
)
232+
233+
client = self.get_client()
234+
result = client.update_email_template(1, 5, template)
235+
236+
assert result == response_body
237+
request = responses.calls[0].request # type: ignore
238+
assert request.body == json.dumps(template.api_data).encode()
239+
240+
@responses.activate
241+
def test_update_email_template_should_raise_error(self) -> None:
242+
template = mt.EmailTemplate(name="Template", subject="s", category="Cat")
243+
responses.add(
244+
responses.PATCH,
245+
self.TEMPLATE_DETAIL_URL,
246+
json={"errors": ["fail"]},
247+
status=401,
248+
)
249+
250+
client = self.get_client()
251+
252+
with pytest.raises(mt.AuthorizationError):
253+
client.update_email_template(1, 5, template)
254+
255+
@responses.activate
256+
def test_delete_email_template_should_return_none(self) -> None:
257+
responses.add(
258+
responses.DELETE,
259+
self.TEMPLATE_DETAIL_URL,
260+
status=204,
261+
)
262+
263+
client = self.get_client()
264+
result = client.delete_email_template(1, 5)
265+
266+
assert result is None
267+
assert len(responses.calls) == 1
268+
269+
@responses.activate
270+
def test_delete_email_template_should_raise_error(self) -> None:
271+
responses.add(
272+
responses.DELETE,
273+
self.TEMPLATE_DETAIL_URL,
274+
json={"errors": ["fail"]},
275+
status=500,
276+
)
277+
278+
client = self.get_client()
279+
280+
with pytest.raises(mt.APIError):
281+
client.delete_email_template(1, 5)

0 commit comments

Comments
 (0)