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.
@@ -0,0 +1,6 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ .venv/
5
+ __pycache__/
6
+ .pytest_cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailflat
3
- Version: 0.1.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.1.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
- dev = ["pytest>=7", "respx>=0.20"]
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"]
@@ -27,7 +27,7 @@ from .errors import (
27
27
  )
28
28
  from .inbox import Inbox, Message
29
29
 
30
- __version__ = "0.1.0"
30
+ __version__ = "0.3.0"
31
31
 
32
32
  __all__ = [
33
33
  "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
- self._meta = meta
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 [Message.from_dict(e) for e in (res.get("emails") or [])]
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 Message.from_dict(email) if email else None
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 Message.from_dict(email)
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