mailflat 0.1.0__tar.gz → 0.3.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.3.0/.gitignore +6 -0
- {mailflat-0.1.0 → mailflat-0.3.0}/PKG-INFO +4 -1
- {mailflat-0.1.0 → mailflat-0.3.0}/pyproject.toml +3 -2
- {mailflat-0.1.0 → mailflat-0.3.0}/src/mailflat/__init__.py +1 -1
- {mailflat-0.1.0 → mailflat-0.3.0}/src/mailflat/inbox.py +27 -4
- mailflat-0.3.0/src/mailflat/langchain.py +155 -0
- mailflat-0.1.0/.gitignore +0 -38
- {mailflat-0.1.0 → mailflat-0.3.0}/LICENSE +0 -0
- {mailflat-0.1.0 → mailflat-0.3.0}/README.md +0 -0
- {mailflat-0.1.0 → mailflat-0.3.0}/src/mailflat/client.py +0 -0
- {mailflat-0.1.0 → mailflat-0.3.0}/src/mailflat/errors.py +0 -0
- {mailflat-0.1.0 → mailflat-0.3.0}/src/mailflat/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailflat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official Python SDK for MailFlat — disposable, automation-friendly email inboxes with one-line OTP retrieval.
|
|
5
5
|
Project-URL: Homepage, https://mailflat.net
|
|
6
6
|
Project-URL: Documentation, https://mailflat.net/docs
|
|
@@ -20,8 +20,11 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.8
|
|
21
21
|
Requires-Dist: httpx>=0.24
|
|
22
22
|
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: langchain-core>=0.2; extra == 'dev'
|
|
23
24
|
Requires-Dist: pytest>=7; extra == 'dev'
|
|
24
25
|
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
26
|
+
Provides-Extra: langchain
|
|
27
|
+
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
25
28
|
Description-Content-Type: text/markdown
|
|
26
29
|
|
|
27
30
|
# MailFlat — Python SDK
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mailflat"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Official Python SDK for MailFlat — disposable, automation-friendly email inboxes with one-line OTP retrieval."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -29,7 +29,8 @@ Documentation = "https://mailflat.net/docs"
|
|
|
29
29
|
Source = "https://github.com/onderyentar/mailflat"
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
|
-
|
|
32
|
+
langchain = ["langchain-core>=0.2"]
|
|
33
|
+
dev = ["pytest>=7", "respx>=0.20", "langchain-core>=0.2"]
|
|
33
34
|
|
|
34
35
|
[tool.hatch.build.targets.wheel]
|
|
35
36
|
packages = ["src/mailflat"]
|
|
@@ -42,6 +42,17 @@ class Message:
|
|
|
42
42
|
send_error: str | None = None
|
|
43
43
|
received_at: str | None = None
|
|
44
44
|
raw: dict[str, Any] = field(default_factory=dict, repr=False)
|
|
45
|
+
# Bu mesajı döndüren Inbox (msg.delete() şekeri için). Inbox.messages()/latest()/wait_* doldurur.
|
|
46
|
+
_inbox: "Inbox | None" = field(default=None, repr=False, compare=False)
|
|
47
|
+
|
|
48
|
+
def delete(self) -> dict[str, Any]:
|
|
49
|
+
"""Bu maili sil (inbox kalır). Mesaj bir Inbox'tan geldiyse çalışır."""
|
|
50
|
+
if self._inbox is None or self.id is None:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"This message isn't attached to an inbox (or has no id). "
|
|
53
|
+
"Use inbox.delete_message(message_id) instead."
|
|
54
|
+
)
|
|
55
|
+
return self._inbox.delete_message(self.id)
|
|
45
56
|
|
|
46
57
|
@classmethod
|
|
47
58
|
def from_dict(cls, data: dict[str, Any]) -> "Message":
|
|
@@ -75,26 +86,34 @@ class Inbox:
|
|
|
75
86
|
self.address: str = address
|
|
76
87
|
# create/list yanıtından gelen ek alanlar (varsa)
|
|
77
88
|
self.name: str | None = meta.get("name")
|
|
89
|
+
self.api_key: str | None = meta.get("api_key") # per-inbox key (mf_sk_...)
|
|
78
90
|
self.retention_hours: int | None = meta.get("retention_hours")
|
|
79
91
|
self.encrypted: bool = bool(meta.get("encrypted"))
|
|
80
92
|
self.via_api: bool | None = meta.get("via_api")
|
|
81
93
|
self.created_at: str | None = meta.get("created_at")
|
|
82
|
-
|
|
94
|
+
# backend yanıtının tamamı (Message.raw ile simetrik) — MCP/agent katmanı için.
|
|
95
|
+
self.raw: dict[str, Any] = {"address": address, **meta}
|
|
83
96
|
|
|
84
97
|
def __repr__(self) -> str:
|
|
85
98
|
return f"<Inbox {self.address!r}>"
|
|
86
99
|
|
|
87
100
|
# ----------------------------------------------------------------- okuma
|
|
101
|
+
def _wrap(self, data: dict[str, Any]) -> Message:
|
|
102
|
+
"""API mail dict'ini Message'a çevirir + bu inbox ref'ini iliştirir (msg.delete() için)."""
|
|
103
|
+
m = Message.from_dict(data)
|
|
104
|
+
m._inbox = self
|
|
105
|
+
return m
|
|
106
|
+
|
|
88
107
|
def messages(self) -> list[Message]:
|
|
89
108
|
"""Inbox'taki tüm mesajları (yeniden eskiye) döndürür."""
|
|
90
109
|
res = self._client._get(f"/api/v1/inboxes/{self.address}/messages")
|
|
91
|
-
return [
|
|
110
|
+
return [self._wrap(e) for e in (res.get("emails") or [])]
|
|
92
111
|
|
|
93
112
|
def latest(self) -> Message | None:
|
|
94
113
|
"""En son mesajı döndürür (yoksa None)."""
|
|
95
114
|
res = self._client._get(f"/api/v1/inboxes/{self.address}/latest")
|
|
96
115
|
email = res.get("email")
|
|
97
|
-
return
|
|
116
|
+
return self._wrap(email) if email else None
|
|
98
117
|
|
|
99
118
|
def wait_for_message(
|
|
100
119
|
self, *, timeout: float = 30, poll_interval: float = 1.0
|
|
@@ -113,7 +132,7 @@ class Inbox:
|
|
|
113
132
|
)
|
|
114
133
|
email = res.get("email")
|
|
115
134
|
if email:
|
|
116
|
-
return
|
|
135
|
+
return self._wrap(email)
|
|
117
136
|
if time.monotonic() >= deadline:
|
|
118
137
|
raise OTPTimeoutError(
|
|
119
138
|
f"No message arrived for {self.address} within {timeout}s"
|
|
@@ -156,3 +175,7 @@ class Inbox:
|
|
|
156
175
|
def delete(self) -> dict[str, Any]:
|
|
157
176
|
"""Inbox'u ve tüm mesajlarını siler. Geri alınamaz."""
|
|
158
177
|
return self._client._delete(f"/api/v1/inboxes/{self.address}")
|
|
178
|
+
|
|
179
|
+
def delete_message(self, message_id: int) -> dict[str, Any]:
|
|
180
|
+
"""Inbox'taki TEK bir maili siler (inbox kalır). `Message.delete()` bunu çağırır."""
|
|
181
|
+
return self._client._delete(f"/api/v1/inboxes/{self.address}/messages/{message_id}")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""MailFlat LangChain entegrasyonu — `MailFlatToolkit` + 6 araçlık LangChain tool seti.
|
|
2
|
+
|
|
3
|
+
`mailflat` SDK'sının üstüne ince bir LangChain kabuğu; tüm HTTP/iş mantığı client'ta (DRY).
|
|
4
|
+
`langchain-core` opsiyonel bağımlılıktır → `pip install mailflat[langchain]`.
|
|
5
|
+
|
|
6
|
+
Kullanım:
|
|
7
|
+
from mailflat.langchain import MailFlatToolkit
|
|
8
|
+
toolkit = MailFlatToolkit(api_key=env("MAILFLAT_KEY"))
|
|
9
|
+
tools = toolkit.get_tools() # create_inbox, list_inboxes, read_messages,
|
|
10
|
+
# wait_for_otp, send_email, delete_inbox
|
|
11
|
+
|
|
12
|
+
Connected to:
|
|
13
|
+
- imports from: mailflat.client (MailFlat), langchain_core.tools (StructuredTool)
|
|
14
|
+
- imported by: kullanıcı kodu (LangChain ajanları)
|
|
15
|
+
|
|
16
|
+
Key exports:
|
|
17
|
+
- `MailFlatToolkit(api_key=..., base_url=...)` — `.get_tools()` → list[BaseTool]
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
25
|
+
except ImportError as exc: # langchain-core kurulu değil → açıklayıcı hata
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"MailFlat's LangChain integration requires 'langchain-core'. "
|
|
28
|
+
"Install it with: pip install mailflat[langchain]"
|
|
29
|
+
) from exc
|
|
30
|
+
|
|
31
|
+
from .client import DEFAULT_BASE_URL, MailFlat
|
|
32
|
+
from .errors import EncryptedInboxError, MailFlatError, OTPTimeoutError
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Sequence
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MailFlatToolkit:
|
|
39
|
+
"""MailFlat araçlarını bir LangChain ajanına bağlamak için toolkit.
|
|
40
|
+
|
|
41
|
+
>>> from mailflat.langchain import MailFlatToolkit
|
|
42
|
+
>>> toolkit = MailFlatToolkit(api_key="mf_live_...")
|
|
43
|
+
>>> tools = toolkit.get_tools()
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
*,
|
|
50
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
51
|
+
client: MailFlat | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
# client enjeksiyonu testleri kolaylaştırır; yoksa api_key ile kurulur.
|
|
54
|
+
self._client = client or MailFlat(api_key=api_key, base_url=base_url)
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------ tools
|
|
57
|
+
def get_tools(self) -> "Sequence[BaseTool]":
|
|
58
|
+
"""6 MailFlat aracını LangChain `BaseTool` listesi olarak döndürür."""
|
|
59
|
+
client = self._client
|
|
60
|
+
|
|
61
|
+
def create_inbox(prefix: str = "", label: str = "", retention_hours: int = 0) -> dict:
|
|
62
|
+
"""Create a real disposable email inbox and return its address.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
prefix: Optional left-hand prefix (e.g. 'signup'); random if empty.
|
|
66
|
+
label: Optional human label to remember what this inbox is for.
|
|
67
|
+
retention_hours: Optional retention in hours; 0 = your plan's max,
|
|
68
|
+
values above your plan are capped.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
inbox = client.create(
|
|
72
|
+
prefix=prefix or None,
|
|
73
|
+
label=label or None,
|
|
74
|
+
retention_hours=retention_hours if retention_hours and retention_hours > 0 else None,
|
|
75
|
+
)
|
|
76
|
+
return inbox.raw
|
|
77
|
+
except MailFlatError as e:
|
|
78
|
+
return {"error": str(e)}
|
|
79
|
+
|
|
80
|
+
def list_inboxes() -> dict:
|
|
81
|
+
"""List all inboxes available to this API key."""
|
|
82
|
+
try:
|
|
83
|
+
return {"ok": True, "inboxes": [i.raw for i in client.list()]}
|
|
84
|
+
except MailFlatError as e:
|
|
85
|
+
return {"error": str(e)}
|
|
86
|
+
|
|
87
|
+
def read_messages(address: str) -> dict:
|
|
88
|
+
"""Read all messages in the given inbox address (newest first).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
address: The full inbox address, e.g. signup-8f3@mailflat.net.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
return {"ok": True, "emails": [m.raw for m in client.inbox(address).messages()]}
|
|
95
|
+
except MailFlatError as e:
|
|
96
|
+
return {"error": str(e)}
|
|
97
|
+
|
|
98
|
+
def wait_for_otp(address: str, timeout: int = 30) -> dict:
|
|
99
|
+
"""Poll the inbox until a one-time verification code (OTP) arrives.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
address: The inbox address to poll.
|
|
103
|
+
timeout: Maximum seconds to wait before giving up (default 30).
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
inbox = client.inbox(address)
|
|
107
|
+
otp = inbox.wait_for_otp(timeout=timeout)
|
|
108
|
+
latest = inbox.latest()
|
|
109
|
+
return {"otp_code": otp, "email": latest.raw if latest else None}
|
|
110
|
+
except OTPTimeoutError:
|
|
111
|
+
return {"otp_code": None, "error": "timeout"}
|
|
112
|
+
except EncryptedInboxError as e:
|
|
113
|
+
return {"otp_code": None, "encrypted": True, "error": str(e)}
|
|
114
|
+
except MailFlatError as e:
|
|
115
|
+
return {"otp_code": None, "error": str(e)}
|
|
116
|
+
|
|
117
|
+
def send_email(
|
|
118
|
+
address: str, to: str, subject: str = "", body: str = "", html: str = ""
|
|
119
|
+
) -> dict:
|
|
120
|
+
"""Send an email FROM the given inbox address (DKIM-signed via MailFlat's MTA).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
address: The inbox address to send from (must belong to this API key).
|
|
124
|
+
to: Recipient email address.
|
|
125
|
+
subject: Email subject line.
|
|
126
|
+
body: Plain-text body.
|
|
127
|
+
html: Optional HTML body.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
return client.inbox(address).send(to, subject=subject, body=body, html=html or None)
|
|
131
|
+
except MailFlatError as e:
|
|
132
|
+
return {"error": str(e)}
|
|
133
|
+
|
|
134
|
+
def delete_inbox(address: str) -> dict:
|
|
135
|
+
"""Delete an inbox and all its messages by address. Irreversible.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
address: The inbox address to delete.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
return client.inbox(address).delete()
|
|
142
|
+
except MailFlatError as e:
|
|
143
|
+
return {"error": str(e)}
|
|
144
|
+
|
|
145
|
+
funcs: list[Any] = [
|
|
146
|
+
create_inbox,
|
|
147
|
+
list_inboxes,
|
|
148
|
+
read_messages,
|
|
149
|
+
wait_for_otp,
|
|
150
|
+
send_email,
|
|
151
|
+
delete_inbox,
|
|
152
|
+
]
|
|
153
|
+
# from_function tip ipuçları + docstring'den args_schema'yı kendisi çıkarır
|
|
154
|
+
# (pydantic v1/v2 sürüm farkı bu yolla atlanır).
|
|
155
|
+
return [StructuredTool.from_function(func=f) for f in funcs]
|
mailflat-0.1.0/.gitignore
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|