mailflat 0.1.0__tar.gz
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.
- mailflat-0.1.0/.gitignore +38 -0
- mailflat-0.1.0/LICENSE +21 -0
- mailflat-0.1.0/PKG-INFO +104 -0
- mailflat-0.1.0/README.md +78 -0
- mailflat-0.1.0/pyproject.toml +38 -0
- mailflat-0.1.0/src/mailflat/__init__.py +45 -0
- mailflat-0.1.0/src/mailflat/client.py +159 -0
- mailflat-0.1.0/src/mailflat/errors.py +66 -0
- mailflat-0.1.0/src/mailflat/inbox.py +158 -0
- mailflat-0.1.0/src/mailflat/py.typed +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
venv/
|
|
5
|
+
.venv/
|
|
6
|
+
*.db
|
|
7
|
+
*.db-wal
|
|
8
|
+
*.db-shm
|
|
9
|
+
|
|
10
|
+
# env — secret'lar asla commit edilmez (.env.example HARİÇ, o şablon)
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
|
|
15
|
+
# secrets — DKIM gizli anahtarı + SSH key'ler
|
|
16
|
+
*.pem
|
|
17
|
+
*_key
|
|
18
|
+
*_key.pub
|
|
19
|
+
id_rsa*
|
|
20
|
+
id_ed25519*
|
|
21
|
+
|
|
22
|
+
# Node / Next.js
|
|
23
|
+
node_modules/
|
|
24
|
+
.next/
|
|
25
|
+
dist/
|
|
26
|
+
out/
|
|
27
|
+
|
|
28
|
+
# Playwright (E2E) — test çıktıları + indirilen browser'lar
|
|
29
|
+
test-results/
|
|
30
|
+
playwright-report/
|
|
31
|
+
playwright/.cache/
|
|
32
|
+
.last-run.json
|
|
33
|
+
|
|
34
|
+
# OS / editor
|
|
35
|
+
.DS_Store
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
/Keys
|
mailflat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MailFlat
|
|
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.
|
mailflat-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailflat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for MailFlat — disposable, automation-friendly email inboxes with one-line OTP retrieval.
|
|
5
|
+
Project-URL: Homepage, https://mailflat.net
|
|
6
|
+
Project-URL: Documentation, https://mailflat.net/docs
|
|
7
|
+
Project-URL: Source, https://github.com/onderyentar/mailflat
|
|
8
|
+
Author-email: MailFlat <support@mailflat.net>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,automation,disposable-email,email,otp,temporary-email,testing
|
|
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
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Communications :: Email
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Requires-Dist: httpx>=0.24
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
24
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# MailFlat — Python SDK
|
|
28
|
+
|
|
29
|
+
Official Python client for [MailFlat](https://mailflat.net): disposable, automation-friendly
|
|
30
|
+
email inboxes with **one-line OTP retrieval**. Spin up a real inbox, read the verification
|
|
31
|
+
code your app just sent, and move on — no flaky polling, no shared mailbox state.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install mailflat
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from mailflat import MailFlat
|
|
41
|
+
|
|
42
|
+
mf = MailFlat(api_key="mf_live_...") # or set MAILFLAT_API_KEY
|
|
43
|
+
|
|
44
|
+
# 1 · spin up a disposable inbox
|
|
45
|
+
inbox = mf.create(label="signup-test")
|
|
46
|
+
print(inbox.address) # → signup-test-8f3@x7k2m.mailflat.net
|
|
47
|
+
|
|
48
|
+
# 2 · your app/browser submits the form using inbox.address ...
|
|
49
|
+
|
|
50
|
+
# 3 · grab the OTP (polls until it arrives or times out)
|
|
51
|
+
otp = inbox.wait_for_otp(timeout=30)
|
|
52
|
+
print(otp) # → "123456"
|
|
53
|
+
|
|
54
|
+
# inbox auto-clears in 2h — no cleanup needed (or call inbox.delete())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## For AI agents
|
|
58
|
+
|
|
59
|
+
Hand an agent one API key and it spins up real inboxes on demand:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
mf = MailFlat() # reads MAILFLAT_API_KEY
|
|
63
|
+
|
|
64
|
+
inbox = mf.create(label="deep-research")
|
|
65
|
+
browser.fill("#email", inbox.address)
|
|
66
|
+
browser.click("Sign up")
|
|
67
|
+
|
|
68
|
+
otp = inbox.wait_for_otp(timeout=30)
|
|
69
|
+
browser.fill("#code", otp)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### `MailFlat(api_key=None, *, base_url="https://mailflat.net", timeout=30.0, max_retries=2)`
|
|
75
|
+
Client. `api_key` falls back to the `MAILFLAT_API_KEY` environment variable. Use `base_url`
|
|
76
|
+
for self-hosted / BYOD deployments. Supports use as a context manager (`with MailFlat() as mf:`).
|
|
77
|
+
|
|
78
|
+
- `create(*, prefix=None, label=None, subdomain=None, domain=None, retention_hours=None) -> Inbox`
|
|
79
|
+
— open a new inbox. `create_inbox(...)` is an alias.
|
|
80
|
+
- `list() -> list[Inbox]` — inboxes opened with this key.
|
|
81
|
+
- `inbox(address) -> Inbox` — attach to an existing address without a network call.
|
|
82
|
+
|
|
83
|
+
### `Inbox`
|
|
84
|
+
- `.address` — the email address.
|
|
85
|
+
- `.messages() -> list[Message]` — all messages, newest first.
|
|
86
|
+
- `.latest() -> Message | None` — most recent message.
|
|
87
|
+
- `.wait_for_otp(*, timeout=30, poll_interval=1.0) -> str` — poll until an OTP arrives; returns the code.
|
|
88
|
+
- `.wait_for_message(*, timeout=30, poll_interval=1.0) -> Message` — poll until any message arrives.
|
|
89
|
+
- `.send(to, *, subject="", body="", html=None) -> dict` — send a DKIM-signed email from this inbox.
|
|
90
|
+
- `.delete() -> dict` — delete the inbox and all its messages.
|
|
91
|
+
|
|
92
|
+
### `Message`
|
|
93
|
+
`.otp`, `.subject`, `.sender`, `.text`, `.html`, `.to_address`, `.direction`, `.received_at`, `.raw`.
|
|
94
|
+
|
|
95
|
+
## Errors
|
|
96
|
+
|
|
97
|
+
All errors subclass `MailFlatError`: `AuthenticationError` (401), `PermissionError` (403),
|
|
98
|
+
`NotFoundError` (404), `RateLimitError` (429), `APIError` (other), `OTPTimeoutError`
|
|
99
|
+
(no OTP before timeout), `EncryptedInboxError` (the inbox is end-to-end encrypted, so the
|
|
100
|
+
server cannot read its contents — use a non-encrypted inbox for agent automation).
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
mailflat-0.1.0/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# MailFlat — Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for [MailFlat](https://mailflat.net): disposable, automation-friendly
|
|
4
|
+
email inboxes with **one-line OTP retrieval**. Spin up a real inbox, read the verification
|
|
5
|
+
code your app just sent, and move on — no flaky polling, no shared mailbox state.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mailflat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from mailflat import MailFlat
|
|
15
|
+
|
|
16
|
+
mf = MailFlat(api_key="mf_live_...") # or set MAILFLAT_API_KEY
|
|
17
|
+
|
|
18
|
+
# 1 · spin up a disposable inbox
|
|
19
|
+
inbox = mf.create(label="signup-test")
|
|
20
|
+
print(inbox.address) # → signup-test-8f3@x7k2m.mailflat.net
|
|
21
|
+
|
|
22
|
+
# 2 · your app/browser submits the form using inbox.address ...
|
|
23
|
+
|
|
24
|
+
# 3 · grab the OTP (polls until it arrives or times out)
|
|
25
|
+
otp = inbox.wait_for_otp(timeout=30)
|
|
26
|
+
print(otp) # → "123456"
|
|
27
|
+
|
|
28
|
+
# inbox auto-clears in 2h — no cleanup needed (or call inbox.delete())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## For AI agents
|
|
32
|
+
|
|
33
|
+
Hand an agent one API key and it spins up real inboxes on demand:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
mf = MailFlat() # reads MAILFLAT_API_KEY
|
|
37
|
+
|
|
38
|
+
inbox = mf.create(label="deep-research")
|
|
39
|
+
browser.fill("#email", inbox.address)
|
|
40
|
+
browser.click("Sign up")
|
|
41
|
+
|
|
42
|
+
otp = inbox.wait_for_otp(timeout=30)
|
|
43
|
+
browser.fill("#code", otp)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `MailFlat(api_key=None, *, base_url="https://mailflat.net", timeout=30.0, max_retries=2)`
|
|
49
|
+
Client. `api_key` falls back to the `MAILFLAT_API_KEY` environment variable. Use `base_url`
|
|
50
|
+
for self-hosted / BYOD deployments. Supports use as a context manager (`with MailFlat() as mf:`).
|
|
51
|
+
|
|
52
|
+
- `create(*, prefix=None, label=None, subdomain=None, domain=None, retention_hours=None) -> Inbox`
|
|
53
|
+
— open a new inbox. `create_inbox(...)` is an alias.
|
|
54
|
+
- `list() -> list[Inbox]` — inboxes opened with this key.
|
|
55
|
+
- `inbox(address) -> Inbox` — attach to an existing address without a network call.
|
|
56
|
+
|
|
57
|
+
### `Inbox`
|
|
58
|
+
- `.address` — the email address.
|
|
59
|
+
- `.messages() -> list[Message]` — all messages, newest first.
|
|
60
|
+
- `.latest() -> Message | None` — most recent message.
|
|
61
|
+
- `.wait_for_otp(*, timeout=30, poll_interval=1.0) -> str` — poll until an OTP arrives; returns the code.
|
|
62
|
+
- `.wait_for_message(*, timeout=30, poll_interval=1.0) -> Message` — poll until any message arrives.
|
|
63
|
+
- `.send(to, *, subject="", body="", html=None) -> dict` — send a DKIM-signed email from this inbox.
|
|
64
|
+
- `.delete() -> dict` — delete the inbox and all its messages.
|
|
65
|
+
|
|
66
|
+
### `Message`
|
|
67
|
+
`.otp`, `.subject`, `.sender`, `.text`, `.html`, `.to_address`, `.direction`, `.received_at`, `.raw`.
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
All errors subclass `MailFlatError`: `AuthenticationError` (401), `PermissionError` (403),
|
|
72
|
+
`NotFoundError` (404), `RateLimitError` (429), `APIError` (other), `OTPTimeoutError`
|
|
73
|
+
(no OTP before timeout), `EncryptedInboxError` (the inbox is end-to-end encrypted, so the
|
|
74
|
+
server cannot read its contents — use a non-encrypted inbox for agent automation).
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mailflat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for MailFlat — disposable, automation-friendly email inboxes with one-line OTP retrieval."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "MailFlat", email = "support@mailflat.net" }]
|
|
13
|
+
keywords = ["email", "disposable-email", "otp", "automation", "testing", "ai-agents", "temporary-email"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Communications :: Email",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["httpx>=0.24"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://mailflat.net"
|
|
28
|
+
Documentation = "https://mailflat.net/docs"
|
|
29
|
+
Source = "https://github.com/onderyentar/mailflat"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=7", "respx>=0.20"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/mailflat"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
include = ["src/mailflat", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""MailFlat Python SDK — disposable, otomasyon-dostu e-posta inbox'ları için resmi client.
|
|
2
|
+
|
|
3
|
+
`pip install mailflat` → `from mailflat import MailFlat`.
|
|
4
|
+
|
|
5
|
+
Connected to:
|
|
6
|
+
- imports from: mailflat.client, mailflat.inbox, mailflat.errors
|
|
7
|
+
- imported by: kullanıcı kodu, mailflat-mcp, mailflat.langchain
|
|
8
|
+
|
|
9
|
+
Key exports:
|
|
10
|
+
- `MailFlat` — client
|
|
11
|
+
- `Inbox`, `Message` — domain tipleri
|
|
12
|
+
- hata tipleri: `MailFlatError`, `AuthenticationError`, `PermissionError`,
|
|
13
|
+
`NotFoundError`, `RateLimitError`, `APIError`, `OTPTimeoutError`, `EncryptedInboxError`
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .client import MailFlat
|
|
18
|
+
from .errors import (
|
|
19
|
+
APIError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
EncryptedInboxError,
|
|
22
|
+
MailFlatError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
OTPTimeoutError,
|
|
25
|
+
PermissionError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
)
|
|
28
|
+
from .inbox import Inbox, Message
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"MailFlat",
|
|
34
|
+
"Inbox",
|
|
35
|
+
"Message",
|
|
36
|
+
"MailFlatError",
|
|
37
|
+
"AuthenticationError",
|
|
38
|
+
"PermissionError",
|
|
39
|
+
"NotFoundError",
|
|
40
|
+
"RateLimitError",
|
|
41
|
+
"APIError",
|
|
42
|
+
"OTPTimeoutError",
|
|
43
|
+
"EncryptedInboxError",
|
|
44
|
+
"__version__",
|
|
45
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""MailFlat client — /api/v1 otomasyon API'sinin ince, tiplenmiş Python sarmalayıcısı.
|
|
2
|
+
|
|
3
|
+
Tek hesap API key'i (X-API-Key) ile inbox açar, listeler, mail/OTP okur ve mail gönderir.
|
|
4
|
+
Tüm üst katmanlar (MCP, LangChain) bu client'ı çağırır → tek kaynak.
|
|
5
|
+
|
|
6
|
+
Connected to:
|
|
7
|
+
- imports from: mailflat.inbox, mailflat.errors (+ httpx)
|
|
8
|
+
- imported by: mailflat.__init__
|
|
9
|
+
|
|
10
|
+
Key exports:
|
|
11
|
+
- `MailFlat(api_key=..., base_url=...)` — client
|
|
12
|
+
- `.create(...) / .create_inbox(...)` — yeni inbox → `Inbox`
|
|
13
|
+
- `.list()` — `Inbox` listesi
|
|
14
|
+
- `.inbox(address)` — mevcut adrese bağlan
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from .errors import MailFlatError, raise_for_status
|
|
25
|
+
from .inbox import Inbox
|
|
26
|
+
|
|
27
|
+
DEFAULT_BASE_URL = "https://mailflat.net"
|
|
28
|
+
_RETRY_STATUSES = {429, 502, 503, 504}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MailFlat:
|
|
32
|
+
"""MailFlat otomasyon API client'ı.
|
|
33
|
+
|
|
34
|
+
>>> from mailflat import MailFlat
|
|
35
|
+
>>> mf = MailFlat(api_key="mf_live_...")
|
|
36
|
+
>>> inbox = mf.create(label="signup-test")
|
|
37
|
+
>>> inbox.address
|
|
38
|
+
'signup-test-...@mailflat.net'
|
|
39
|
+
>>> otp = inbox.wait_for_otp(timeout=30)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
api_key: str | None = None,
|
|
45
|
+
*,
|
|
46
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
max_retries: int = 2,
|
|
49
|
+
http_client: httpx.Client | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
api_key = api_key or os.environ.get("MAILFLAT_API_KEY")
|
|
52
|
+
if not api_key:
|
|
53
|
+
raise MailFlatError(
|
|
54
|
+
"An API key is required. Pass MailFlat(api_key=...) or set MAILFLAT_API_KEY."
|
|
55
|
+
)
|
|
56
|
+
self.api_key = api_key
|
|
57
|
+
self.base_url = base_url.rstrip("/")
|
|
58
|
+
self.max_retries = max(0, max_retries)
|
|
59
|
+
# http_client enjeksiyonu testleri (httpx.MockTransport) kolaylaştırır.
|
|
60
|
+
self._http = http_client or httpx.Client(
|
|
61
|
+
base_url=self.base_url,
|
|
62
|
+
headers={"X-API-Key": api_key},
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------- context
|
|
67
|
+
def __enter__(self) -> "MailFlat":
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, *exc: object) -> None:
|
|
71
|
+
self.close()
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
self._http.close()
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------- HTTP iç
|
|
77
|
+
def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
78
|
+
attempt = 0
|
|
79
|
+
while True:
|
|
80
|
+
try:
|
|
81
|
+
resp = self._http.request(method, path, json=json)
|
|
82
|
+
except httpx.HTTPError as exc: # ağ/timeout
|
|
83
|
+
raise MailFlatError(f"Network error: {exc}") from exc
|
|
84
|
+
|
|
85
|
+
if resp.status_code in _RETRY_STATUSES and attempt < self.max_retries:
|
|
86
|
+
attempt += 1
|
|
87
|
+
time.sleep(min(2 ** attempt * 0.5, 4.0)) # 1s, 2s, ... backoff
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = resp.json()
|
|
92
|
+
except ValueError:
|
|
93
|
+
data = {}
|
|
94
|
+
|
|
95
|
+
if resp.status_code >= 400:
|
|
96
|
+
detail = ""
|
|
97
|
+
if isinstance(data, dict):
|
|
98
|
+
detail = data.get("detail") or data.get("error") or ""
|
|
99
|
+
raise_for_status(resp.status_code, detail)
|
|
100
|
+
|
|
101
|
+
if isinstance(data, dict) and data.get("error"): # güvenlik: 200 + error
|
|
102
|
+
raise MailFlatError(data["error"], status_code=resp.status_code)
|
|
103
|
+
return data if isinstance(data, dict) else {}
|
|
104
|
+
|
|
105
|
+
def _get(self, path: str) -> dict[str, Any]:
|
|
106
|
+
return self._request("GET", path)
|
|
107
|
+
|
|
108
|
+
def _post(self, path: str, json: dict[str, Any]) -> dict[str, Any]:
|
|
109
|
+
return self._request("POST", path, json=json)
|
|
110
|
+
|
|
111
|
+
def _delete(self, path: str) -> dict[str, Any]:
|
|
112
|
+
return self._request("DELETE", path)
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------------------- public
|
|
115
|
+
def create(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
prefix: str | None = None,
|
|
119
|
+
label: str | None = None,
|
|
120
|
+
subdomain: str | None = None,
|
|
121
|
+
domain: str | None = None,
|
|
122
|
+
retention_hours: int | None = None,
|
|
123
|
+
) -> Inbox:
|
|
124
|
+
"""Yeni bir disposable inbox açar ve `Inbox` döndürür.
|
|
125
|
+
|
|
126
|
+
- prefix: adresin local kısmı (boşsa rastgele `agent-...`)
|
|
127
|
+
- label: inbox'a verilen isim (UI'da görünür; bazı planlarda yok sayılır)
|
|
128
|
+
- subdomain: mailflat.net alt alanı (boşsa rastgele)
|
|
129
|
+
- domain: BYOD doğrulanmış kendi domain'in (örn. acme.com)
|
|
130
|
+
- retention_hours: mesaj saklama süresi (boş → plan varsayılanı; plan max'ı ile sınırlı)
|
|
131
|
+
"""
|
|
132
|
+
payload: dict[str, Any] = {}
|
|
133
|
+
if prefix is not None:
|
|
134
|
+
payload["prefix"] = prefix
|
|
135
|
+
if label is not None:
|
|
136
|
+
payload["label"] = label
|
|
137
|
+
if subdomain is not None:
|
|
138
|
+
payload["subdomain"] = subdomain
|
|
139
|
+
if domain is not None:
|
|
140
|
+
payload["domain"] = domain
|
|
141
|
+
if retention_hours is not None:
|
|
142
|
+
payload["retention_hours"] = retention_hours
|
|
143
|
+
res = self._post("/api/v1/inboxes", payload)
|
|
144
|
+
return Inbox(self, res["address"], **{k: v for k, v in res.items() if k != "address"})
|
|
145
|
+
|
|
146
|
+
# Landing snippet'leriyle birebir uyum için alias.
|
|
147
|
+
create_inbox = create
|
|
148
|
+
|
|
149
|
+
def list(self) -> list[Inbox]:
|
|
150
|
+
"""Bu API key ile açılmış (agent) inbox'ları döndürür."""
|
|
151
|
+
res = self._get("/api/v1/inboxes")
|
|
152
|
+
return [
|
|
153
|
+
Inbox(self, i["address"], **{k: v for k, v in i.items() if k != "address"})
|
|
154
|
+
for i in (res.get("inboxes") or [])
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
def inbox(self, address: str) -> Inbox:
|
|
158
|
+
"""Var olan bir adrese (API çağrısı yapmadan) bağlan."""
|
|
159
|
+
return Inbox(self, address)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""MailFlat SDK hata tipleri — HTTP durum kodlarını anlamlı Python exception'larına çevirir.
|
|
2
|
+
|
|
3
|
+
Connected to:
|
|
4
|
+
- imported by: mailflat.client, mailflat.inbox, mailflat.__init__
|
|
5
|
+
|
|
6
|
+
Key exports:
|
|
7
|
+
- `MailFlatError` — tüm SDK hatalarının tabanı
|
|
8
|
+
- `AuthenticationError` — 401 (geçersiz/eksik API key)
|
|
9
|
+
- `PermissionError` — 403 (key bu inbox'a sahip değil)
|
|
10
|
+
- `NotFoundError` — 404
|
|
11
|
+
- `RateLimitError` — 429
|
|
12
|
+
- `APIError` — diğer 4xx/5xx
|
|
13
|
+
- `OTPTimeoutError` — wait_for_otp süre aşımı
|
|
14
|
+
- `EncryptedInboxError` — E2E inbox içeriği API'den okunamaz
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MailFlatError(Exception):
|
|
20
|
+
"""Tüm MailFlat SDK hatalarının tabanı."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthenticationError(MailFlatError):
|
|
29
|
+
"""401 — API key eksik veya geçersiz."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PermissionError(MailFlatError): # noqa: A001 - kasıtlı, SDK kapsamında net isim
|
|
33
|
+
"""403 — API key bu inbox'a sahip değil."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotFoundError(MailFlatError):
|
|
37
|
+
"""404 — kaynak bulunamadı."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RateLimitError(MailFlatError):
|
|
41
|
+
"""429 — rate limit aşıldı."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class APIError(MailFlatError):
|
|
45
|
+
"""Beklenmeyen 4xx/5xx yanıtı."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class OTPTimeoutError(MailFlatError):
|
|
49
|
+
"""wait_for_otp verilen süre içinde OTP bulamadı."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EncryptedInboxError(MailFlatError):
|
|
53
|
+
"""Inbox uçtan uca şifreli — sunucu içeriği okuyamaz, OTP/gövde API'den alınamaz."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def raise_for_status(status_code: int, detail: str) -> None:
|
|
57
|
+
"""HTTP durum kodunu uygun MailFlatError alt tipine çevirip fırlatır."""
|
|
58
|
+
if status_code == 401:
|
|
59
|
+
raise AuthenticationError(detail or "Invalid or missing API key", status_code=status_code)
|
|
60
|
+
if status_code == 403:
|
|
61
|
+
raise PermissionError(detail or "This API key does not own that inbox", status_code=status_code)
|
|
62
|
+
if status_code == 404:
|
|
63
|
+
raise NotFoundError(detail or "Not found", status_code=status_code)
|
|
64
|
+
if status_code == 429:
|
|
65
|
+
raise RateLimitError(detail or "Rate limit exceeded", status_code=status_code)
|
|
66
|
+
raise APIError(detail or f"Request failed with status {status_code}", status_code=status_code)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Inbox + Message — bir MailFlat inbox'ı ve mesajları üzerindeki yüksek seviye işlemler.
|
|
2
|
+
|
|
3
|
+
`Inbox`, `MailFlat` client tarafından döndürülür; mesaj okuma, OTP bekleme, gönderme ve
|
|
4
|
+
silme metodlarını taşır. `Message`, /api/v1 e-posta yanıtının tiplenmiş hâlidir.
|
|
5
|
+
|
|
6
|
+
Connected to:
|
|
7
|
+
- imports from: mailflat.errors
|
|
8
|
+
- imported by: mailflat.client, mailflat.__init__
|
|
9
|
+
|
|
10
|
+
Key exports:
|
|
11
|
+
- `Inbox` — `.address`, `.messages()`, `.latest()`, `.wait_for_otp()`, `.send()`, `.delete()`
|
|
12
|
+
- `Message` — `.otp`, `.subject`, `.text`, `.html`, `.sender`, ...
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from .errors import EncryptedInboxError, OTPTimeoutError
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING: # döngüsel import'tan kaçın (sadece tip ipucu)
|
|
23
|
+
from .client import MailFlat
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Message:
|
|
28
|
+
"""Tek bir e-posta — /api/v1 serialize çıktısının tiplenmiş hâli."""
|
|
29
|
+
|
|
30
|
+
id: int | None = None
|
|
31
|
+
sender: str | None = None
|
|
32
|
+
subject: str | None = None
|
|
33
|
+
text: str | None = None # body_text
|
|
34
|
+
html: str | None = None # body_html
|
|
35
|
+
otp: str | None = None # otp_code (sunucu çıkardıysa)
|
|
36
|
+
tag: str | None = None
|
|
37
|
+
to_address: str | None = None
|
|
38
|
+
is_encrypted: bool = False
|
|
39
|
+
direction: str | None = None # "in" | "out"
|
|
40
|
+
is_read: bool = False
|
|
41
|
+
send_status: str | None = None
|
|
42
|
+
send_error: str | None = None
|
|
43
|
+
received_at: str | None = None
|
|
44
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, data: dict[str, Any]) -> "Message":
|
|
48
|
+
return cls(
|
|
49
|
+
id=data.get("id"),
|
|
50
|
+
sender=data.get("sender"),
|
|
51
|
+
subject=data.get("subject"),
|
|
52
|
+
text=data.get("body_text"),
|
|
53
|
+
html=data.get("body_html"),
|
|
54
|
+
otp=data.get("otp_code"),
|
|
55
|
+
tag=data.get("tag"),
|
|
56
|
+
to_address=data.get("to_address"),
|
|
57
|
+
is_encrypted=bool(data.get("is_encrypted")),
|
|
58
|
+
direction=data.get("direction"),
|
|
59
|
+
is_read=bool(data.get("is_read")),
|
|
60
|
+
send_status=data.get("send_status"),
|
|
61
|
+
send_error=data.get("send_error"),
|
|
62
|
+
received_at=data.get("received_at"),
|
|
63
|
+
raw=data,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Inbox:
|
|
68
|
+
"""Tek bir disposable inbox üzerindeki işlemler.
|
|
69
|
+
|
|
70
|
+
Doğrudan oluşturmak yerine `MailFlat.create()` / `.list()` / `.inbox(address)` ile alınır.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, client: "MailFlat", address: str, **meta: Any) -> None:
|
|
74
|
+
self._client = client
|
|
75
|
+
self.address: str = address
|
|
76
|
+
# create/list yanıtından gelen ek alanlar (varsa)
|
|
77
|
+
self.name: str | None = meta.get("name")
|
|
78
|
+
self.retention_hours: int | None = meta.get("retention_hours")
|
|
79
|
+
self.encrypted: bool = bool(meta.get("encrypted"))
|
|
80
|
+
self.via_api: bool | None = meta.get("via_api")
|
|
81
|
+
self.created_at: str | None = meta.get("created_at")
|
|
82
|
+
self._meta = meta
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return f"<Inbox {self.address!r}>"
|
|
86
|
+
|
|
87
|
+
# ----------------------------------------------------------------- okuma
|
|
88
|
+
def messages(self) -> list[Message]:
|
|
89
|
+
"""Inbox'taki tüm mesajları (yeniden eskiye) döndürür."""
|
|
90
|
+
res = self._client._get(f"/api/v1/inboxes/{self.address}/messages")
|
|
91
|
+
return [Message.from_dict(e) for e in (res.get("emails") or [])]
|
|
92
|
+
|
|
93
|
+
def latest(self) -> Message | None:
|
|
94
|
+
"""En son mesajı döndürür (yoksa None)."""
|
|
95
|
+
res = self._client._get(f"/api/v1/inboxes/{self.address}/latest")
|
|
96
|
+
email = res.get("email")
|
|
97
|
+
return Message.from_dict(email) if email else None
|
|
98
|
+
|
|
99
|
+
def wait_for_message(
|
|
100
|
+
self, *, timeout: float = 30, poll_interval: float = 1.0
|
|
101
|
+
) -> Message:
|
|
102
|
+
"""Yeni bir mesaj gelene kadar poll'lar; gelince `Message` döndürür.
|
|
103
|
+
|
|
104
|
+
timeout aşılırsa `OTPTimeoutError`, inbox E2E şifreliyse `EncryptedInboxError`.
|
|
105
|
+
"""
|
|
106
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
107
|
+
while True:
|
|
108
|
+
res = self._client._get(f"/api/v1/inboxes/{self.address}/latest")
|
|
109
|
+
if res.get("encrypted"):
|
|
110
|
+
raise EncryptedInboxError(
|
|
111
|
+
res.get("note") or "This inbox is end-to-end encrypted; "
|
|
112
|
+
"use a non-encrypted inbox for agent automation."
|
|
113
|
+
)
|
|
114
|
+
email = res.get("email")
|
|
115
|
+
if email:
|
|
116
|
+
return Message.from_dict(email)
|
|
117
|
+
if time.monotonic() >= deadline:
|
|
118
|
+
raise OTPTimeoutError(
|
|
119
|
+
f"No message arrived for {self.address} within {timeout}s"
|
|
120
|
+
)
|
|
121
|
+
time.sleep(poll_interval)
|
|
122
|
+
|
|
123
|
+
def wait_for_otp(self, *, timeout: float = 30, poll_interval: float = 1.0) -> str:
|
|
124
|
+
"""OTP kodu gelene kadar poll'lar ve kodu (str) döndürür.
|
|
125
|
+
|
|
126
|
+
timeout aşılırsa `OTPTimeoutError`, inbox E2E şifreliyse `EncryptedInboxError`.
|
|
127
|
+
"""
|
|
128
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
129
|
+
while True:
|
|
130
|
+
res = self._client._get(f"/api/v1/inboxes/{self.address}/latest")
|
|
131
|
+
if res.get("encrypted"):
|
|
132
|
+
raise EncryptedInboxError(
|
|
133
|
+
res.get("note") or "This inbox is end-to-end encrypted; "
|
|
134
|
+
"OTP cannot be read via the API."
|
|
135
|
+
)
|
|
136
|
+
email = res.get("email") or {}
|
|
137
|
+
otp = email.get("otp_code")
|
|
138
|
+
if otp:
|
|
139
|
+
return otp
|
|
140
|
+
if time.monotonic() >= deadline:
|
|
141
|
+
raise OTPTimeoutError(
|
|
142
|
+
f"No OTP arrived for {self.address} within {timeout}s"
|
|
143
|
+
)
|
|
144
|
+
time.sleep(poll_interval)
|
|
145
|
+
|
|
146
|
+
# ----------------------------------------------------------------- yazma
|
|
147
|
+
def send(
|
|
148
|
+
self, to: str, *, subject: str = "", body: str = "", html: str | None = None
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""Bu inbox adresinden mail gönderir (DKIM imzalı, kendi MTA'mız üzerinden)."""
|
|
151
|
+
payload: dict[str, Any] = {"to": to, "subject": subject, "body": body}
|
|
152
|
+
if html is not None:
|
|
153
|
+
payload["html"] = html
|
|
154
|
+
return self._client._post(f"/api/v1/inboxes/{self.address}/send", payload)
|
|
155
|
+
|
|
156
|
+
def delete(self) -> dict[str, Any]:
|
|
157
|
+
"""Inbox'u ve tüm mesajlarını siler. Geri alınamaz."""
|
|
158
|
+
return self._client._delete(f"/api/v1/inboxes/{self.address}")
|
|
File without changes
|