Skip to content

Commit 39c1f13

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fix issue #26: Add MessagesApi, releated models, examples, tests
1 parent 22b1e1d commit 39c1f13

File tree

7 files changed

+1263
-3
lines changed

7 files changed

+1263
-3
lines changed

examples/testing/messages.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Optional
2+
3+
import mailtrap as mt
4+
from mailtrap.models.messages import AnalysisReport
5+
from mailtrap.models.messages import EmailMessage
6+
from mailtrap.models.messages import ForwardedMessage
7+
8+
API_TOKEN = "YOU_API_TOKEN"
9+
ACCOUNT_ID = "YOU_ACCOUNT_ID"
10+
INBOX_ID = "YOUR_INBOX_ID"
11+
12+
client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID)
13+
messages_api = client.testing_api.messages
14+
15+
16+
def get_message(inbox_id: int, message_id: int) -> EmailMessage:
17+
return messages_api.show_message(inbox_id=inbox_id, message_id=message_id)
18+
19+
20+
def update_message(inbox_id: int, message_id: int, is_read: bool) -> EmailMessage:
21+
return messages_api.update(
22+
inbox_id=inbox_id,
23+
message_id=message_id,
24+
message_params=mt.UpdateEmailMessageParams(is_read=is_read),
25+
)
26+
27+
28+
def delete_message(inbox_id: int, message_id: int) -> EmailMessage:
29+
return messages_api.delete(inbox_id=inbox_id, message_id=message_id)
30+
31+
32+
def list_messages(
33+
inbox_id: int,
34+
search: Optional[str] = None,
35+
last_id: Optional[int] = None,
36+
page: Optional[int] = None,
37+
) -> list[EmailMessage]:
38+
return messages_api.get_list(
39+
inbox_id=inbox_id, search=search, last_id=last_id, page=page
40+
)
41+
42+
43+
def forward_message(inbox_id: int, message_id: int, email: str) -> ForwardedMessage:
44+
return messages_api.forward(inbox_id=inbox_id, message_id=message_id, email=email)
45+
46+
47+
def get_spam_report(inbox_id: int, message_id: str):
48+
return messages_api.get_spam_report(inbox_id=inbox_id, message_id=message_id)
49+
50+
51+
def get_html_analysis(inbox_id: int, message_id: str) -> AnalysisReport:
52+
return messages_api.get_html_analysis(inbox_id=inbox_id, message_id=message_id)
53+
54+
55+
def get_text_body(inbox_id: int, message_id: str) -> str:
56+
return messages_api.get_text_body(inbox_id=inbox_id, message_id=message_id)
57+
58+
59+
def get_raw_body(inbox_id: int, message_id: str) -> str:
60+
return messages_api.get_raw_body(inbox_id=inbox_id, message_id=message_id)
61+
62+
63+
def get_html_source(inbox_id: int, message_id: str) -> str:
64+
return messages_api.get_html_source(inbox_id=inbox_id, message_id=message_id)
65+
66+
67+
def get_html_body(inbox_id: int, message_id: str) -> str:
68+
return messages_api.get_html_body(inbox_id=inbox_id, message_id=message_id)
69+
70+
71+
def get_eml_body(inbox_id: int, message_id: str) -> str:
72+
return messages_api.get_eml_body(inbox_id=inbox_id, message_id=message_id)
73+
74+
75+
def get_mail_headers(inbox_id: int, message_id: str) -> str:
76+
return messages_api.get_mail_headers(inbox_id=inbox_id, message_id=message_id)
77+
78+
79+
if __name__ == "__main__":
80+
messages = list_messages(inbox_id=INBOX_ID)
81+
print(messages)

mailtrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .models.mail import Disposition
1919
from .models.mail import Mail
2020
from .models.mail import MailFromTemplate
21+
from .models.messages import UpdateEmailMessageParams
2122
from .models.projects import ProjectParams
2223
from .models.templates import CreateEmailTemplateParams
2324
from .models.templates import UpdateEmailTemplateParams

mailtrap/api/resources/messages.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from typing import Any
2+
from typing import Optional
3+
from typing import cast
4+
5+
from mailtrap.http import HttpClient
6+
from mailtrap.models.messages import AnalysisReport
7+
from mailtrap.models.messages import AnalysisReportResponse
8+
from mailtrap.models.messages import EmailMessage
9+
from mailtrap.models.messages import ForwardedMessage
10+
from mailtrap.models.messages import SpamReport
11+
from mailtrap.models.messages import UpdateEmailMessageParams
12+
13+
14+
class MessagesApi:
15+
def __init__(self, client: HttpClient, account_id: str) -> None:
16+
self._account_id = account_id
17+
self._client = client
18+
19+
def show_message(self, inbox_id: int, message_id: int) -> EmailMessage:
20+
"""Get email message by ID."""
21+
response = self._client.get(self._api_path(inbox_id, message_id))
22+
return EmailMessage(**response)
23+
24+
def update(
25+
self, inbox_id: int, message_id: int, message_params: UpdateEmailMessageParams
26+
) -> EmailMessage:
27+
"""
28+
Update message attributes
29+
(right now only the **is_read** attribute is available for modification).
30+
"""
31+
response = self._client.patch(
32+
self._api_path(inbox_id, message_id),
33+
json={"message": message_params.api_data},
34+
)
35+
return EmailMessage(**response)
36+
37+
def delete(self, inbox_id: int, message_id: int) -> EmailMessage:
38+
"""Delete message from inbox."""
39+
response = self._client.delete(self._api_path(inbox_id, message_id))
40+
return EmailMessage(**response)
41+
42+
def get_list(
43+
self,
44+
inbox_id: int,
45+
search: Optional[str] = None,
46+
last_id: Optional[int] = None,
47+
page: Optional[int] = None,
48+
) -> list[EmailMessage]:
49+
"""
50+
Get messages from the inbox.
51+
52+
The response contains up to 30 messages per request. You can use pagination
53+
parameters (`last_id` or `page`) to retrieve additional results.
54+
55+
Args:
56+
inbox_id (int): ID of the inbox to retrieve messages from.
57+
search (Optional[str]):
58+
Search query string. Matches `subject`, `to_email`, and `to_name`.
59+
Example: `"welcome"`
60+
last_id (Optional[int]):
61+
If specified, returns a page of records before the given `last_id`.
62+
Overrides `page` if both are provided.
63+
Must be `>= 1`.
64+
Example: `123`
65+
page (Optional[int]):
66+
Page number for paginated results.
67+
Ignored if `last_id` is also provided.
68+
Must be `>= 1`.
69+
Example: `5`
70+
71+
Returns:
72+
list[EmailMessage]: A list of email messages.
73+
74+
Notes:
75+
- Only one of `last_id` or `page` should typically be used.
76+
- `last_id` has higher priority if both are provided.
77+
- Each response contains at most 30 messages.
78+
"""
79+
params: dict[str, Any] = {}
80+
if search:
81+
params["search"] = search
82+
if last_id:
83+
params["last_id"] = last_id
84+
if page:
85+
params["page"] = page
86+
87+
response = self._client.get(self._api_path(inbox_id), params=params)
88+
return [EmailMessage(**message) for message in response]
89+
90+
def forward(self, inbox_id: int, message_id: int, email: str) -> ForwardedMessage:
91+
"""
92+
Forward message to an email address.
93+
The email address must be confirmed by the recipient in advance.
94+
"""
95+
response = self._client.post(
96+
f"{self._api_path(inbox_id, message_id)}/forward", json={"email": email}
97+
)
98+
return ForwardedMessage(**response)
99+
100+
def get_spam_report(self, inbox_id: int, message_id: int) -> SpamReport:
101+
"""Get a brief spam report by message ID."""
102+
response = self._client.get(f"{self._api_path(inbox_id, message_id)}/spam_report")
103+
return SpamReport(**response["report"])
104+
105+
def get_html_analysis(self, inbox_id: int, message_id: int) -> AnalysisReport:
106+
"""Get a brief HTML report by message ID."""
107+
response = self._client.get(f"{self._api_path(inbox_id, message_id)}/analyze")
108+
return AnalysisReportResponse(**response).report
109+
110+
def get_text_body(self, inbox_id: int, message_id: int) -> str:
111+
"""Get text email body, if it exists."""
112+
return cast(
113+
str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.txt")
114+
)
115+
116+
def get_raw_body(self, inbox_id: int, message_id: int) -> str:
117+
"""Get raw email body."""
118+
return cast(
119+
str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.raw")
120+
)
121+
122+
def get_html_source(self, inbox_id: int, message_id: int) -> str:
123+
"""Get HTML source of email."""
124+
return cast(
125+
str,
126+
self._client.get(f"{self._api_path(inbox_id, message_id)}/body.htmlsource"),
127+
)
128+
129+
def get_html_body(self, inbox_id: int, message_id: int) -> str:
130+
"""Get formatted HTML email body. Not applicable for plain text emails."""
131+
return cast(
132+
str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.html")
133+
)
134+
135+
def get_eml_body(self, inbox_id: int, message_id: int) -> str:
136+
"""Get email message in .eml format."""
137+
return cast(
138+
str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.eml")
139+
)
140+
141+
def get_mail_headers(self, inbox_id: int, message_id: int) -> dict[str, Any]:
142+
"""Get mail headers of a message."""
143+
response = self._client.get(
144+
f"{self._api_path(inbox_id, message_id)}/mail_headers"
145+
)
146+
return cast(dict[str, Any], response["headers"])
147+
148+
def _api_path(self, inbox_id: int, message_id: Optional[int] = None) -> str:
149+
path = f"/api/accounts/{self._account_id}/inboxes/{inbox_id}/messages"
150+
if message_id:
151+
return f"{path}/{message_id}"
152+
return path

mailtrap/api/testing.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22

33
from mailtrap.api.resources.inboxes import InboxesApi
4+
from mailtrap.api.resources.messages import MessagesApi
45
from mailtrap.api.resources.projects import ProjectsApi
56
from mailtrap.http import HttpClient
67

@@ -20,3 +21,7 @@ def projects(self) -> ProjectsApi:
2021
@property
2122
def inboxes(self) -> InboxesApi:
2223
return InboxesApi(account_id=self._account_id, client=self._client)
24+
25+
@property
26+
def messages(self) -> MessagesApi:
27+
return MessagesApi(account_id=self._account_id, client=self._client)

mailtrap/http.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import NoReturn
33
from typing import Optional
44

5+
from requests import JSONDecodeError
56
from requests import Response
67
from requests import Session
78

@@ -54,14 +55,23 @@ def _process_response(self, response: Response) -> Any:
5455
if not response.content.strip():
5556
return None
5657

57-
return response.json()
58+
try:
59+
return response.json()
60+
except (JSONDecodeError, ValueError):
61+
return response.text
5862

5963
def _handle_failed_response(self, response: Response) -> NoReturn:
6064
status_code = response.status_code
65+
66+
if not response.content:
67+
if status_code == 404:
68+
raise APIError(status_code, errors=["Not Found"])
69+
raise APIError(status_code, errors=["Empty response body"])
70+
6171
try:
6272
data = response.json()
63-
except ValueError as exc:
64-
raise APIError(status_code, errors=["Unknown Error"]) from exc
73+
except (JSONDecodeError, ValueError) as exc:
74+
raise APIError(status_code, errors=["Invalid JSON"]) from exc
6575

6676
errors = self._extract_errors(data)
6777

0 commit comments

Comments
 (0)