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.
@@ -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.
@@ -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
@@ -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