mails-agent 1.4.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mails_agent/__init__.py +18 -0
- mails_agent/client.py +408 -0
- mails_agent/exceptions.py +27 -0
- mails_agent/models.py +43 -0
- mails_agent-1.4.0b1.dist-info/METADATA +231 -0
- mails_agent-1.4.0b1.dist-info/RECORD +8 -0
- mails_agent-1.4.0b1.dist-info/WHEEL +4 -0
- mails_agent-1.4.0b1.dist-info/licenses/LICENSE +21 -0
mails_agent/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""mails-agent — Python SDK for email capabilities for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .client import AsyncMailsClient, MailsClient
|
|
4
|
+
from .exceptions import ApiError, AuthError, MailsError, NotFoundError
|
|
5
|
+
from .models import Email, SendResult, VerificationCode
|
|
6
|
+
|
|
7
|
+
__version__ = "1.4.0b1"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MailsClient",
|
|
10
|
+
"AsyncMailsClient",
|
|
11
|
+
"Email",
|
|
12
|
+
"SendResult",
|
|
13
|
+
"VerificationCode",
|
|
14
|
+
"MailsError",
|
|
15
|
+
"AuthError",
|
|
16
|
+
"NotFoundError",
|
|
17
|
+
"ApiError",
|
|
18
|
+
]
|
mails_agent/client.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Synchronous and asynchronous HTTP clients for the mails-agent API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import ApiError, AuthError, NotFoundError
|
|
10
|
+
from .models import Email, SendResult, VerificationCode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_email(data: Dict[str, Any]) -> Email:
|
|
14
|
+
"""Convert a raw API dict into an Email dataclass."""
|
|
15
|
+
return Email(
|
|
16
|
+
id=data["id"],
|
|
17
|
+
mailbox=data.get("mailbox", ""),
|
|
18
|
+
from_address=data.get("from_address", ""),
|
|
19
|
+
from_name=data.get("from_name", ""),
|
|
20
|
+
subject=data.get("subject", ""),
|
|
21
|
+
direction=data.get("direction", "inbound"),
|
|
22
|
+
status=data.get("status", ""),
|
|
23
|
+
received_at=data.get("received_at", ""),
|
|
24
|
+
has_attachments=bool(data.get("has_attachments", False)),
|
|
25
|
+
attachment_count=int(data.get("attachment_count", 0)),
|
|
26
|
+
body_text=data.get("body_text", ""),
|
|
27
|
+
body_html=data.get("body_html", ""),
|
|
28
|
+
code=data.get("code"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _handle_error(response: httpx.Response) -> None:
|
|
33
|
+
"""Raise the appropriate exception for non-2xx responses."""
|
|
34
|
+
if response.status_code == 401 or response.status_code == 403:
|
|
35
|
+
raise AuthError(f"Authentication failed ({response.status_code})")
|
|
36
|
+
if response.status_code == 404:
|
|
37
|
+
raise NotFoundError("Resource not found")
|
|
38
|
+
if not response.is_success:
|
|
39
|
+
try:
|
|
40
|
+
body = response.json()
|
|
41
|
+
message = body.get("error", response.reason_phrase)
|
|
42
|
+
except Exception:
|
|
43
|
+
message = response.reason_phrase or f"HTTP {response.status_code}"
|
|
44
|
+
raise ApiError(message, response.status_code)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MailsClient:
|
|
48
|
+
"""Synchronous client for the mails-agent API.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
api_url: Worker API base URL (e.g. ``https://mails-worker.genedai.workers.dev``).
|
|
52
|
+
token: API key or worker token for authentication.
|
|
53
|
+
mailbox: Your email address (e.g. ``agent@mails0.com``).
|
|
54
|
+
timeout: Request timeout in seconds. Defaults to 60 to accommodate
|
|
55
|
+
long-polling ``wait_for_code`` calls.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
api_url: str,
|
|
61
|
+
token: str,
|
|
62
|
+
mailbox: str,
|
|
63
|
+
*,
|
|
64
|
+
timeout: float = 60.0,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.api_url = api_url.rstrip("/")
|
|
67
|
+
self.token = token
|
|
68
|
+
self.mailbox = mailbox
|
|
69
|
+
self._client = httpx.Client(
|
|
70
|
+
base_url=self.api_url,
|
|
71
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Context manager support
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def __enter__(self) -> "MailsClient":
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, *args: Any) -> None:
|
|
83
|
+
self.close()
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
"""Close the underlying HTTP client."""
|
|
87
|
+
self._client.close()
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Public API
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def send(
|
|
94
|
+
self,
|
|
95
|
+
to: Union[str, List[str]],
|
|
96
|
+
subject: str,
|
|
97
|
+
*,
|
|
98
|
+
text: Optional[str] = None,
|
|
99
|
+
html: Optional[str] = None,
|
|
100
|
+
reply_to: Optional[str] = None,
|
|
101
|
+
attachments: Optional[Sequence[Dict[str, Any]]] = None,
|
|
102
|
+
) -> SendResult:
|
|
103
|
+
"""Send an email.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
to: Recipient address or list of addresses.
|
|
107
|
+
subject: Email subject line.
|
|
108
|
+
text: Plain-text body.
|
|
109
|
+
html: HTML body.
|
|
110
|
+
reply_to: Reply-to address.
|
|
111
|
+
attachments: List of attachment dicts with ``filename``, ``content``,
|
|
112
|
+
and optionally ``content_type`` / ``content_id``.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A :class:`SendResult` with the message id and provider info.
|
|
116
|
+
"""
|
|
117
|
+
recipients = [to] if isinstance(to, str) else list(to)
|
|
118
|
+
payload: Dict[str, Any] = {
|
|
119
|
+
"from": self.mailbox,
|
|
120
|
+
"to": recipients,
|
|
121
|
+
"subject": subject,
|
|
122
|
+
}
|
|
123
|
+
if text is not None:
|
|
124
|
+
payload["text"] = text
|
|
125
|
+
if html is not None:
|
|
126
|
+
payload["html"] = html
|
|
127
|
+
if reply_to is not None:
|
|
128
|
+
payload["reply_to"] = reply_to
|
|
129
|
+
if attachments:
|
|
130
|
+
payload["attachments"] = list(attachments)
|
|
131
|
+
|
|
132
|
+
response = self._client.post("/api/send", json=payload)
|
|
133
|
+
_handle_error(response)
|
|
134
|
+
data = response.json()
|
|
135
|
+
return SendResult(
|
|
136
|
+
id=data["id"],
|
|
137
|
+
provider=data.get("provider", ""),
|
|
138
|
+
provider_id=data.get("provider_id"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def get_inbox(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
limit: int = 20,
|
|
145
|
+
offset: int = 0,
|
|
146
|
+
direction: Optional[str] = None,
|
|
147
|
+
query: Optional[str] = None,
|
|
148
|
+
) -> List[Email]:
|
|
149
|
+
"""Fetch emails from the inbox.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
limit: Maximum number of emails to return.
|
|
153
|
+
offset: Pagination offset.
|
|
154
|
+
direction: Filter by ``'inbound'`` or ``'outbound'``.
|
|
155
|
+
query: Optional search query string.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A list of :class:`Email` objects.
|
|
159
|
+
"""
|
|
160
|
+
params: Dict[str, Any] = {
|
|
161
|
+
"to": self.mailbox,
|
|
162
|
+
"limit": limit,
|
|
163
|
+
"offset": offset,
|
|
164
|
+
}
|
|
165
|
+
if direction is not None:
|
|
166
|
+
params["direction"] = direction
|
|
167
|
+
if query is not None:
|
|
168
|
+
params["query"] = query
|
|
169
|
+
|
|
170
|
+
response = self._client.get("/api/inbox", params=params)
|
|
171
|
+
_handle_error(response)
|
|
172
|
+
data = response.json()
|
|
173
|
+
return [_parse_email(e) for e in data.get("emails", [])]
|
|
174
|
+
|
|
175
|
+
def search(
|
|
176
|
+
self,
|
|
177
|
+
query: str,
|
|
178
|
+
*,
|
|
179
|
+
limit: int = 20,
|
|
180
|
+
direction: Optional[str] = None,
|
|
181
|
+
) -> List[Email]:
|
|
182
|
+
"""Search emails by query string.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
query: Search query.
|
|
186
|
+
limit: Maximum number of results.
|
|
187
|
+
direction: Filter by ``'inbound'`` or ``'outbound'``.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
A list of matching :class:`Email` objects.
|
|
191
|
+
"""
|
|
192
|
+
return self.get_inbox(query=query, limit=limit, direction=direction)
|
|
193
|
+
|
|
194
|
+
def get_email(self, email_id: str) -> Email:
|
|
195
|
+
"""Fetch a single email by ID.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
email_id: The email's unique identifier.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The :class:`Email` object.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
NotFoundError: If the email does not exist.
|
|
205
|
+
"""
|
|
206
|
+
response = self._client.get("/api/email", params={"id": email_id})
|
|
207
|
+
_handle_error(response)
|
|
208
|
+
return _parse_email(response.json())
|
|
209
|
+
|
|
210
|
+
def wait_for_code(
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
timeout: int = 30,
|
|
214
|
+
) -> Optional[VerificationCode]:
|
|
215
|
+
"""Wait for a verification code to arrive.
|
|
216
|
+
|
|
217
|
+
This long-polls the server until a code is found or the timeout
|
|
218
|
+
expires.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
timeout: Maximum seconds to wait. Defaults to 30.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A :class:`VerificationCode` if one arrived, or ``None`` on timeout.
|
|
225
|
+
"""
|
|
226
|
+
# Use a longer HTTP timeout to cover the server-side polling window.
|
|
227
|
+
response = self._client.get(
|
|
228
|
+
"/api/code",
|
|
229
|
+
params={"to": self.mailbox, "timeout": timeout},
|
|
230
|
+
timeout=max(timeout + 10, 60.0),
|
|
231
|
+
)
|
|
232
|
+
_handle_error(response)
|
|
233
|
+
data = response.json()
|
|
234
|
+
if not data.get("code"):
|
|
235
|
+
return None
|
|
236
|
+
return VerificationCode(
|
|
237
|
+
code=data["code"],
|
|
238
|
+
from_address=data.get("from", ""),
|
|
239
|
+
subject=data.get("subject", ""),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def delete_email(self, email_id: str) -> bool:
|
|
243
|
+
"""Delete an email by ID.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
email_id: The email's unique identifier.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
``True`` if the email was deleted, ``False`` if it was not found.
|
|
250
|
+
"""
|
|
251
|
+
response = self._client.delete("/api/email", params={"id": email_id})
|
|
252
|
+
if response.status_code == 404:
|
|
253
|
+
return False
|
|
254
|
+
_handle_error(response)
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ======================================================================
|
|
259
|
+
# Async client
|
|
260
|
+
# ======================================================================
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class AsyncMailsClient:
|
|
264
|
+
"""Asynchronous client for the mails-agent API.
|
|
265
|
+
|
|
266
|
+
Mirrors :class:`MailsClient` but all methods are ``async``.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
api_url: Worker API base URL.
|
|
270
|
+
token: API key or worker token for authentication.
|
|
271
|
+
mailbox: Your email address.
|
|
272
|
+
timeout: Request timeout in seconds (default 60).
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
api_url: str,
|
|
278
|
+
token: str,
|
|
279
|
+
mailbox: str,
|
|
280
|
+
*,
|
|
281
|
+
timeout: float = 60.0,
|
|
282
|
+
) -> None:
|
|
283
|
+
self.api_url = api_url.rstrip("/")
|
|
284
|
+
self.token = token
|
|
285
|
+
self.mailbox = mailbox
|
|
286
|
+
self._client = httpx.AsyncClient(
|
|
287
|
+
base_url=self.api_url,
|
|
288
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
289
|
+
timeout=timeout,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def __aenter__(self) -> "AsyncMailsClient":
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
296
|
+
await self.close()
|
|
297
|
+
|
|
298
|
+
async def close(self) -> None:
|
|
299
|
+
"""Close the underlying HTTP client."""
|
|
300
|
+
await self._client.aclose()
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------
|
|
303
|
+
# Public API
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
async def send(
|
|
307
|
+
self,
|
|
308
|
+
to: Union[str, List[str]],
|
|
309
|
+
subject: str,
|
|
310
|
+
*,
|
|
311
|
+
text: Optional[str] = None,
|
|
312
|
+
html: Optional[str] = None,
|
|
313
|
+
reply_to: Optional[str] = None,
|
|
314
|
+
attachments: Optional[Sequence[Dict[str, Any]]] = None,
|
|
315
|
+
) -> SendResult:
|
|
316
|
+
"""Send an email. See :meth:`MailsClient.send` for details."""
|
|
317
|
+
recipients = [to] if isinstance(to, str) else list(to)
|
|
318
|
+
payload: Dict[str, Any] = {
|
|
319
|
+
"from": self.mailbox,
|
|
320
|
+
"to": recipients,
|
|
321
|
+
"subject": subject,
|
|
322
|
+
}
|
|
323
|
+
if text is not None:
|
|
324
|
+
payload["text"] = text
|
|
325
|
+
if html is not None:
|
|
326
|
+
payload["html"] = html
|
|
327
|
+
if reply_to is not None:
|
|
328
|
+
payload["reply_to"] = reply_to
|
|
329
|
+
if attachments:
|
|
330
|
+
payload["attachments"] = list(attachments)
|
|
331
|
+
|
|
332
|
+
response = await self._client.post("/api/send", json=payload)
|
|
333
|
+
_handle_error(response)
|
|
334
|
+
data = response.json()
|
|
335
|
+
return SendResult(
|
|
336
|
+
id=data["id"],
|
|
337
|
+
provider=data.get("provider", ""),
|
|
338
|
+
provider_id=data.get("provider_id"),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
async def get_inbox(
|
|
342
|
+
self,
|
|
343
|
+
*,
|
|
344
|
+
limit: int = 20,
|
|
345
|
+
offset: int = 0,
|
|
346
|
+
direction: Optional[str] = None,
|
|
347
|
+
query: Optional[str] = None,
|
|
348
|
+
) -> List[Email]:
|
|
349
|
+
"""Fetch emails from the inbox. See :meth:`MailsClient.get_inbox`."""
|
|
350
|
+
params: Dict[str, Any] = {
|
|
351
|
+
"to": self.mailbox,
|
|
352
|
+
"limit": limit,
|
|
353
|
+
"offset": offset,
|
|
354
|
+
}
|
|
355
|
+
if direction is not None:
|
|
356
|
+
params["direction"] = direction
|
|
357
|
+
if query is not None:
|
|
358
|
+
params["query"] = query
|
|
359
|
+
|
|
360
|
+
response = await self._client.get("/api/inbox", params=params)
|
|
361
|
+
_handle_error(response)
|
|
362
|
+
data = response.json()
|
|
363
|
+
return [_parse_email(e) for e in data.get("emails", [])]
|
|
364
|
+
|
|
365
|
+
async def search(
|
|
366
|
+
self,
|
|
367
|
+
query: str,
|
|
368
|
+
*,
|
|
369
|
+
limit: int = 20,
|
|
370
|
+
direction: Optional[str] = None,
|
|
371
|
+
) -> List[Email]:
|
|
372
|
+
"""Search emails by query. See :meth:`MailsClient.search`."""
|
|
373
|
+
return await self.get_inbox(query=query, limit=limit, direction=direction)
|
|
374
|
+
|
|
375
|
+
async def get_email(self, email_id: str) -> Email:
|
|
376
|
+
"""Fetch a single email by ID. See :meth:`MailsClient.get_email`."""
|
|
377
|
+
response = await self._client.get("/api/email", params={"id": email_id})
|
|
378
|
+
_handle_error(response)
|
|
379
|
+
return _parse_email(response.json())
|
|
380
|
+
|
|
381
|
+
async def wait_for_code(
|
|
382
|
+
self,
|
|
383
|
+
*,
|
|
384
|
+
timeout: int = 30,
|
|
385
|
+
) -> Optional[VerificationCode]:
|
|
386
|
+
"""Wait for a verification code. See :meth:`MailsClient.wait_for_code`."""
|
|
387
|
+
response = await self._client.get(
|
|
388
|
+
"/api/code",
|
|
389
|
+
params={"to": self.mailbox, "timeout": timeout},
|
|
390
|
+
timeout=max(timeout + 10, 60.0),
|
|
391
|
+
)
|
|
392
|
+
_handle_error(response)
|
|
393
|
+
data = response.json()
|
|
394
|
+
if not data.get("code"):
|
|
395
|
+
return None
|
|
396
|
+
return VerificationCode(
|
|
397
|
+
code=data["code"],
|
|
398
|
+
from_address=data.get("from", ""),
|
|
399
|
+
subject=data.get("subject", ""),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
async def delete_email(self, email_id: str) -> bool:
|
|
403
|
+
"""Delete an email by ID. See :meth:`MailsClient.delete_email`."""
|
|
404
|
+
response = await self._client.delete("/api/email", params={"id": email_id})
|
|
405
|
+
if response.status_code == 404:
|
|
406
|
+
return False
|
|
407
|
+
_handle_error(response)
|
|
408
|
+
return True
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Custom exceptions for the mails-agent SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MailsError(Exception):
|
|
5
|
+
"""Base exception for mails-agent SDK."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthError(MailsError):
|
|
11
|
+
"""Authentication failed (HTTP 401/403)."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotFoundError(MailsError):
|
|
17
|
+
"""Resource not found (HTTP 404)."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApiError(MailsError):
|
|
23
|
+
"""API returned an error response."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, status_code: int) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.status_code = status_code
|
mails_agent/models.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Data models for the mails-agent SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Email:
|
|
11
|
+
"""Represents an email message."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
mailbox: str
|
|
15
|
+
from_address: str
|
|
16
|
+
from_name: str
|
|
17
|
+
subject: str
|
|
18
|
+
direction: str # 'inbound' | 'outbound'
|
|
19
|
+
status: str
|
|
20
|
+
received_at: str
|
|
21
|
+
has_attachments: bool = False
|
|
22
|
+
attachment_count: int = 0
|
|
23
|
+
body_text: str = ""
|
|
24
|
+
body_html: str = ""
|
|
25
|
+
code: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SendResult:
|
|
30
|
+
"""Result of a send operation."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
provider: str
|
|
34
|
+
provider_id: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class VerificationCode:
|
|
39
|
+
"""A verification code extracted from an email."""
|
|
40
|
+
|
|
41
|
+
code: str
|
|
42
|
+
from_address: str
|
|
43
|
+
subject: str
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mails-agent
|
|
3
|
+
Version: 1.4.0b1
|
|
4
|
+
Summary: Python SDK for mails-agent — email capabilities for AI agents
|
|
5
|
+
Project-URL: Homepage, https://mails0.com
|
|
6
|
+
Project-URL: Repository, https://github.com/Digidai/mails-python
|
|
7
|
+
Project-URL: Documentation, https://github.com/Digidai/mails-python#readme
|
|
8
|
+
Author: Gene Dai
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agent,email,mails,verification-code
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: httpx>=0.24.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# mails-agent
|
|
21
|
+
|
|
22
|
+
Python SDK for [mails0.com](https://mails0.com) -- email capabilities for AI agents.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install mails-agent
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from mails_agent import MailsClient
|
|
34
|
+
|
|
35
|
+
client = MailsClient(
|
|
36
|
+
api_url="https://mails-worker.your-domain.com",
|
|
37
|
+
token="your-api-token",
|
|
38
|
+
mailbox="agent@mails0.com",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Send an email
|
|
42
|
+
result = client.send(
|
|
43
|
+
to="user@example.com",
|
|
44
|
+
subject="Hello from my agent",
|
|
45
|
+
text="This email was sent by an AI agent.",
|
|
46
|
+
)
|
|
47
|
+
print(f"Sent: {result.id}")
|
|
48
|
+
|
|
49
|
+
# Check inbox
|
|
50
|
+
emails = client.get_inbox(limit=5)
|
|
51
|
+
for email in emails:
|
|
52
|
+
print(f"{email.from_address}: {email.subject}")
|
|
53
|
+
|
|
54
|
+
# Wait for a verification code (long-polls up to 30s)
|
|
55
|
+
code = client.wait_for_code(timeout=30)
|
|
56
|
+
if code:
|
|
57
|
+
print(f"Got code: {code.code}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API reference
|
|
61
|
+
|
|
62
|
+
### `MailsClient(api_url, token, mailbox, *, timeout=60.0)`
|
|
63
|
+
|
|
64
|
+
Create a synchronous client. Supports use as a context manager:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
with MailsClient(api_url, token, mailbox) as client:
|
|
68
|
+
emails = client.get_inbox()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `send(to, subject, *, text=None, html=None, reply_to=None, attachments=None) -> SendResult`
|
|
74
|
+
|
|
75
|
+
Send an email. `to` can be a single address or a list.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
result = client.send(
|
|
79
|
+
to=["alice@example.com", "bob@example.com"],
|
|
80
|
+
subject="Team update",
|
|
81
|
+
html="<h1>Update</h1><p>Everything is on track.</p>",
|
|
82
|
+
reply_to="noreply@mails0.com",
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Attachments** are passed as a list of dicts:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
client.send(
|
|
90
|
+
to="user@example.com",
|
|
91
|
+
subject="Report",
|
|
92
|
+
text="See attached.",
|
|
93
|
+
attachments=[{
|
|
94
|
+
"filename": "report.pdf",
|
|
95
|
+
"content": base64_encoded_string,
|
|
96
|
+
"content_type": "application/pdf",
|
|
97
|
+
}],
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### `get_inbox(*, limit=20, offset=0, direction=None, query=None) -> list[Email]`
|
|
104
|
+
|
|
105
|
+
Fetch emails from the inbox with optional filtering.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Get latest 10 inbound emails
|
|
109
|
+
emails = client.get_inbox(limit=10, direction="inbound")
|
|
110
|
+
|
|
111
|
+
# Search for emails containing "invoice"
|
|
112
|
+
emails = client.get_inbox(query="invoice")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `search(query, *, limit=20, direction=None) -> list[Email]`
|
|
118
|
+
|
|
119
|
+
Search emails by query string. Convenience wrapper around `get_inbox`.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
results = client.search("verification code", limit=5)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### `get_email(email_id) -> Email`
|
|
128
|
+
|
|
129
|
+
Fetch a single email by its ID. Raises `NotFoundError` if it does not exist.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
email = client.get_email("abc-123")
|
|
133
|
+
print(email.body_text)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### `wait_for_code(*, timeout=30) -> VerificationCode | None`
|
|
139
|
+
|
|
140
|
+
Long-poll the server for a verification code. Returns `None` if no code arrives within the timeout.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
code = client.wait_for_code(timeout=60)
|
|
144
|
+
if code:
|
|
145
|
+
print(f"Code: {code.code}, From: {code.from_address}")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `delete_email(email_id) -> bool`
|
|
151
|
+
|
|
152
|
+
Delete an email. Returns `True` if deleted, `False` if not found.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
deleted = client.delete_email("abc-123")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Async usage
|
|
159
|
+
|
|
160
|
+
All methods are available as `async` via `AsyncMailsClient`:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import asyncio
|
|
164
|
+
from mails_agent import AsyncMailsClient
|
|
165
|
+
|
|
166
|
+
async def main():
|
|
167
|
+
async with AsyncMailsClient(
|
|
168
|
+
api_url="https://mails-worker.your-domain.com",
|
|
169
|
+
token="your-api-token",
|
|
170
|
+
mailbox="agent@mails0.com",
|
|
171
|
+
) as client:
|
|
172
|
+
# Send
|
|
173
|
+
result = await client.send("user@example.com", "Hello", text="Hi!")
|
|
174
|
+
|
|
175
|
+
# Inbox
|
|
176
|
+
emails = await client.get_inbox()
|
|
177
|
+
|
|
178
|
+
# Wait for code
|
|
179
|
+
code = await client.wait_for_code(timeout=30)
|
|
180
|
+
|
|
181
|
+
asyncio.run(main())
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Data models
|
|
185
|
+
|
|
186
|
+
### `Email`
|
|
187
|
+
|
|
188
|
+
| Field | Type | Description |
|
|
189
|
+
|-------|------|-------------|
|
|
190
|
+
| `id` | `str` | Unique email ID |
|
|
191
|
+
| `mailbox` | `str` | Mailbox address |
|
|
192
|
+
| `from_address` | `str` | Sender email |
|
|
193
|
+
| `from_name` | `str` | Sender display name |
|
|
194
|
+
| `subject` | `str` | Subject line |
|
|
195
|
+
| `direction` | `str` | `"inbound"` or `"outbound"` |
|
|
196
|
+
| `status` | `str` | `"received"`, `"sent"`, `"failed"`, `"queued"` |
|
|
197
|
+
| `received_at` | `str` | ISO 8601 timestamp |
|
|
198
|
+
| `has_attachments` | `bool` | Whether email has attachments |
|
|
199
|
+
| `attachment_count` | `int` | Number of attachments |
|
|
200
|
+
| `body_text` | `str` | Plain text body |
|
|
201
|
+
| `body_html` | `str` | HTML body |
|
|
202
|
+
| `code` | `str \| None` | Extracted verification code, if any |
|
|
203
|
+
|
|
204
|
+
### `SendResult`
|
|
205
|
+
|
|
206
|
+
| Field | Type | Description |
|
|
207
|
+
|-------|------|-------------|
|
|
208
|
+
| `id` | `str` | Message ID |
|
|
209
|
+
| `provider` | `str` | Send provider used |
|
|
210
|
+
| `provider_id` | `str \| None` | Provider-specific ID |
|
|
211
|
+
|
|
212
|
+
### `VerificationCode`
|
|
213
|
+
|
|
214
|
+
| Field | Type | Description |
|
|
215
|
+
|-------|------|-------------|
|
|
216
|
+
| `code` | `str` | The verification code |
|
|
217
|
+
| `from_address` | `str` | Sender of the code email |
|
|
218
|
+
| `subject` | `str` | Subject of the code email |
|
|
219
|
+
|
|
220
|
+
## Exceptions
|
|
221
|
+
|
|
222
|
+
| Exception | When |
|
|
223
|
+
|-----------|------|
|
|
224
|
+
| `MailsError` | Base class for all SDK errors |
|
|
225
|
+
| `AuthError` | 401 or 403 response |
|
|
226
|
+
| `NotFoundError` | 404 response |
|
|
227
|
+
| `ApiError` | Any other non-2xx response (has `.status_code`) |
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mails_agent/__init__.py,sha256=8Qm9eTDhl9s4s76uhGyaFTLVFjLS9xRzaPAdFUJIuBA,458
|
|
2
|
+
mails_agent/client.py,sha256=pJiovsmHz-0bmFd9nkkFr79uC1SH7YRMWcL8TBwE6dU,12984
|
|
3
|
+
mails_agent/exceptions.py,sha256=oJau3nBfIXi5r1s1Tv1vJa5Ec_udzg52w971AaVaDf4,521
|
|
4
|
+
mails_agent/models.py,sha256=sNbZyOO22snc53EfBABawsocBObxi6xLOqSntcChm1Y,811
|
|
5
|
+
mails_agent-1.4.0b1.dist-info/METADATA,sha256=0rJfUIBxlAJaqdgGAksn0q4BIZsvVxXp_0xj66tUQ98,5623
|
|
6
|
+
mails_agent-1.4.0b1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
mails_agent-1.4.0b1.dist-info/licenses/LICENSE,sha256=TPvRtAS8APyBl4qODyF_s2fUg5i1NPrR5I540nqbQhk,1065
|
|
8
|
+
mails_agent-1.4.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Gene Dai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|