mailflat 0.4.0__tar.gz → 0.4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailflat
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -14,10 +14,16 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
17
23
  Classifier: Topic :: Communications :: Email
18
24
  Classifier: Topic :: Software Development :: Testing
19
25
  Classifier: Typing :: Typed
20
- Requires-Python: >=3.8
26
+ Requires-Python: >=3.9
21
27
  Requires-Dist: httpx<1,>=0.24
22
28
  Provides-Extra: dev
23
29
  Requires-Dist: langchain-core>=0.2; extra == 'dev'
@@ -105,9 +111,14 @@ for self-hosted / BYOD deployments. Supports use as a context manager (`with Mai
105
111
 
106
112
  - `.links` — URLs found in the body (HTML hrefs first). For "click the verification link" flows.
107
113
  - `.attachments` — metadata; `msg.attachments[0].download()` fetches the bytes.
114
+ ⚠️ `send()` cannot attach files yet, so you cannot produce an attachment from the SDK
115
+ alone — send one from a normal mail client to test this path.
108
116
  - `.spam` — `{score, required, is_spam, rules, scanner}`, or `None` when never scanned
109
- (which is not the same as a score of 0).
110
- - `.headers` — raw headers; `Message-ID` lives here (threading, dedup).
117
+ (which is not the same as a score of 0). ⚠️ Spam scanning is currently **disabled** on
118
+ mailflat.net, so today this is `None` for every message.
119
+ - `.headers` — raw headers, exactly as they arrived. Names are case-insensitive per
120
+ RFC 5322 but this is a plain dict, so **do not index it**: the real key is `Message-ID`,
121
+ and `headers["Message-Id"]` raises `KeyError`. Use `.header("message-id")` or `.message_id`.
111
122
  - `.mark_read()` / `.delete()` — act on this message directly.
112
123
 
113
124
  ## Errors
@@ -76,9 +76,14 @@ for self-hosted / BYOD deployments. Supports use as a context manager (`with Mai
76
76
 
77
77
  - `.links` — URLs found in the body (HTML hrefs first). For "click the verification link" flows.
78
78
  - `.attachments` — metadata; `msg.attachments[0].download()` fetches the bytes.
79
+ ⚠️ `send()` cannot attach files yet, so you cannot produce an attachment from the SDK
80
+ alone — send one from a normal mail client to test this path.
79
81
  - `.spam` — `{score, required, is_spam, rules, scanner}`, or `None` when never scanned
80
- (which is not the same as a score of 0).
81
- - `.headers` — raw headers; `Message-ID` lives here (threading, dedup).
82
+ (which is not the same as a score of 0). ⚠️ Spam scanning is currently **disabled** on
83
+ mailflat.net, so today this is `None` for every message.
84
+ - `.headers` — raw headers, exactly as they arrived. Names are case-insensitive per
85
+ RFC 5322 but this is a plain dict, so **do not index it**: the real key is `Message-ID`,
86
+ and `headers["Message-Id"]` raises `KeyError`. Use `.header("message-id")` or `.message_id`.
82
87
  - `.mark_read()` / `.delete()` — act on this message directly.
83
88
 
84
89
  ## Errors
@@ -10,7 +10,7 @@ name = "mailflat"
10
10
  dynamic = ["version"]
11
11
  description = "Official Python SDK for MailFlat — disposable, automation-friendly email inboxes with one-line OTP retrieval."
12
12
  readme = "README.md"
13
- requires-python = ">=3.8"
13
+ requires-python = ">=3.9"
14
14
  license = { text = "MIT" }
15
15
  authors = [{ name = "MailFlat", email = "support@mailflat.net" }]
16
16
  keywords = ["email", "disposable-email", "otp", "automation", "testing", "ai-agents", "temporary-email"]
@@ -20,6 +20,16 @@ classifiers = [
20
20
  "License :: OSI Approved :: MIT License",
21
21
  "Programming Language :: Python :: 3",
22
22
  "Programming Language :: Python :: 3 :: Only",
23
+ # Aralığın İKİ UCU gerçekten koşuldu (3.11 + 3.14, 53 test yeşil). Aradaki sürümler için
24
+ # ayrı bir iddia yok: sürüme özgü API kullanmıyoruz ve `from __future__ import annotations`
25
+ # her dosyada var. Alt sınır 3.8 → 3.9: 3.8 EOL ve HİÇ test edilmedi; test edilmemiş bir
26
+ # sürümü desteklediğini söylemek, PyPI'da kullanıcıyı yanlış filtrelemeye götürür.
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
23
33
  "Topic :: Communications :: Email",
24
34
  "Topic :: Software Development :: Testing",
25
35
  "Typing :: Typed",
@@ -2,8 +2,6 @@
2
2
 
3
3
  `pip install mailflat` → `from mailflat import MailFlat`.
4
4
 
5
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
6
-
7
5
  Connected to:
8
6
  - imports from: mailflat.client, mailflat.inbox, mailflat.errors
9
7
  - imported by: user code, mailflat-mcp, mailflat.langchain
@@ -15,6 +13,12 @@ Key exports:
15
13
  `NotFoundError`, `RateLimitError`, `APIError`, `OTPTimeoutError`, `EncryptedInboxError`
16
14
  - `redact_secrets()` — strips secret fields from agent tool output (used by MCP/LangChain)
17
15
  """
16
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
17
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
18
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
19
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
20
+ # Bu satır bilerek YORUM: docstring'e Türkçe not koymak, tam da kaçındığımız şeydi —
21
+ # `help()` docstring'i basar, yorumu basmaz.
18
22
  from __future__ import annotations
19
23
 
20
24
  from .client import MailFlat
@@ -34,7 +38,7 @@ from .redact import redact_secrets
34
38
 
35
39
  # Paketin TEK sürüm kaynağı — pyproject.toml bunu [tool.hatch.version] ile okur.
36
40
  # Yayın = burayı yükselt, başka hiçbir yeri değil.
37
- __version__ = "0.4.0"
41
+ __version__ = "0.4.1"
38
42
 
39
43
  __all__ = [
40
44
  "MailFlat",
@@ -3,8 +3,6 @@
3
3
  One account API key (X-API-Key) creates inboxes, lists them, reads mail/OTP and sends mail.
4
4
  Every higher layer (MCP, LangChain) calls this client, so behaviour lives in one place.
5
5
 
6
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
7
-
8
6
  Connected to:
9
7
  - imports from: mailflat.inbox, mailflat.errors (+ httpx)
10
8
  - imported by: mailflat.__init__
@@ -15,6 +13,12 @@ Key exports:
15
13
  - `.list()` — list of `Inbox`
16
14
  - `.inbox(address)` — attach to an existing address
17
15
  """
16
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
17
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
18
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
19
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
20
+ # Bu satır bilerek YORUM: docstring'e Türkçe not koymak, tam da kaçındığımız şeydi —
21
+ # `help()` docstring'i basar, yorumu basmaz.
18
22
  from __future__ import annotations
19
23
 
20
24
  import logging
@@ -41,11 +45,9 @@ logger = logging.getLogger("mailflat")
41
45
 
42
46
 
43
47
  def _retry_after_seconds(resp: httpx.Response) -> float | None:
44
- """Parse the `Retry-After` header (seconds form). Returns None when absent/unparsable.
45
-
46
- Bir sunucu "12 saniye bekle" diyorsa 1 saniyede geri dönmek, hem 429'u tekrar yemek
47
- hem de rate limit penceresini uzatmak demek.
48
- """
48
+ """Parse the `Retry-After` header (seconds form). Returns None when absent/unparsable."""
49
+ # Sunucu "12 saniye bekle" derken 1 saniyede dönmek hem 429'u tekrar yemek hem de
50
+ # rate limit penceresini uzatmak demekti.
49
51
  raw = resp.headers.get("Retry-After")
50
52
  if not raw:
51
53
  return None
@@ -95,9 +97,9 @@ class MailFlat:
95
97
  def user_agent() -> str:
96
98
  """Return the `User-Agent` this client sends, e.g. `mailflat-python/0.4.0`.
97
99
 
98
- Sunucu tarafında hangi SDK sürümünün sorun çıkardığını görebilmek için; sürüm
99
- döngüsel import'a girmeden okunur.
100
+ Sending the version lets the server tell which SDK build is misbehaving.
100
101
  """
102
+ # Sürüm burada okunuyor ki modül seviyesinde döngüsel import oluşmasın.
101
103
  from . import __version__
102
104
 
103
105
  return f"mailflat-python/{__version__}"
@@ -113,7 +115,13 @@ class MailFlat:
113
115
  self._http.close()
114
116
 
115
117
  # --------------------------------------------------------------- HTTP iç
116
- def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> dict[str, Any]:
118
+ def _request(self, method: str, path: str, json: dict[str, Any] | None = None,
119
+ *, idempotent: bool | None = None) -> dict[str, Any]:
120
+ """`idempotent=True` → metodu ne olursa olsun retry edilebilir sayılır.
121
+
122
+ mark_read/burn POST ama tekrarı zararsız (aynı sonuç). send için ASLA verilmez:
123
+ tekrarlanan bir send aynı maili ikinci kez teslim eder (B-058).
124
+ """
117
125
  attempt = 0
118
126
  while True:
119
127
  try:
@@ -122,9 +130,10 @@ class MailFlat:
122
130
  raise MailFlatError(f"Network error: {exc}") from exc
123
131
 
124
132
  retry_after = _retry_after_seconds(resp)
133
+ can_repeat = method.upper() in _IDEMPOTENT_METHODS if idempotent is None else idempotent
125
134
  retriable = (
126
135
  resp.status_code in _RETRY_STATUSES
127
- and method.upper() in _IDEMPOTENT_METHODS
136
+ and can_repeat
128
137
  and attempt < self.max_retries
129
138
  )
130
139
  if retriable:
@@ -155,18 +164,34 @@ class MailFlat:
155
164
  def _get(self, path: str) -> dict[str, Any]:
156
165
  return self._request("GET", path)
157
166
 
158
- def _post(self, path: str, json: dict[str, Any] | None = None) -> dict[str, Any]:
159
- return self._request("POST", path, json=json or {})
167
+ def _post(self, path: str, json: dict[str, Any] | None = None,
168
+ *, idempotent: bool = False) -> dict[str, Any]:
169
+ return self._request("POST", path, json=json or {}, idempotent=idempotent)
160
170
 
161
171
  def _delete(self, path: str) -> dict[str, Any]:
162
172
  return self._request("DELETE", path)
163
173
 
164
174
  def _get_bytes(self, path: str) -> httpx.Response:
165
- """Raw response for binary endpoints (attachment download)."""
166
- try:
167
- resp = self._http.request("GET", path)
168
- except httpx.HTTPError as exc:
169
- raise MailFlatError(f"Network error: {exc}") from exc
175
+ """Raw response for binary endpoints (attachment download).
176
+
177
+ Retries on the same statuses as any other GET: this path bypassed the retry loop
178
+ entirely until 0.4.1, so an attachment download gave up on the first 503 while
179
+ every other read retried.
180
+ """
181
+ attempt = 0
182
+ while True:
183
+ try:
184
+ resp = self._http.request("GET", path)
185
+ except httpx.HTTPError as exc:
186
+ raise MailFlatError(f"Network error: {exc}") from exc
187
+ retry_after = _retry_after_seconds(resp)
188
+ if resp.status_code in _RETRY_STATUSES and attempt < self.max_retries:
189
+ attempt += 1
190
+ delay = retry_after if retry_after is not None else min(2 ** attempt * 0.5, _MAX_BACKOFF)
191
+ logger.debug("retrying GET %s after %s (status %s)", path, delay, resp.status_code)
192
+ time.sleep(min(delay, _MAX_BACKOFF))
193
+ continue
194
+ break
170
195
  if resp.status_code >= 400:
171
196
  detail = ""
172
197
  try:
@@ -1,8 +1,5 @@
1
1
  """MailFlat SDK error types — maps HTTP status codes to meaningful Python exceptions.
2
2
 
3
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
4
- Burada docstring bir yorum değil, kullanıcının `help()` ve IDE tooltip'inde gördüğü arayüzdür.
5
-
6
3
  Connected to:
7
4
  - imported by: mailflat.client, mailflat.inbox, mailflat.__init__
8
5
 
@@ -16,6 +13,9 @@ Key exports:
16
13
  - `OTPTimeoutError` — wait_for_otp / wait_for_message timed out
17
14
  - `EncryptedInboxError` — end-to-end encrypted inbox cannot be read through the API
18
15
  """
16
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
17
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
18
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
19
19
  from __future__ import annotations
20
20
 
21
21
 
@@ -3,8 +3,6 @@
3
3
  `Inbox` is returned by the `MailFlat` client and carries the read, wait, send and delete
4
4
  methods. `Message` is the typed form of an /api/v1 email payload.
5
5
 
6
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
7
-
8
6
  Connected to:
9
7
  - imports from: mailflat.errors
10
8
  - imported by: mailflat.client, mailflat.__init__
@@ -15,6 +13,9 @@ Key exports:
15
13
  - `Message` — `.otp`, `.subject`, `.text`, `.html`, `.links`, `.attachments`, `.spam`, ...
16
14
  - `Attachment` — `.filename`, `.size_bytes`, `.download()`
17
15
  """
16
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
17
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
18
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
18
19
  from __future__ import annotations
19
20
 
20
21
  import time
@@ -26,8 +27,8 @@ from .errors import EncryptedInboxError, MailFlatError, OTPTimeoutError
26
27
  if TYPE_CHECKING: # döngüsel import'tan kaçın (sadece tip ipucu)
27
28
  from .client import MailFlat
28
29
 
29
- # Sunucunun kabul ettiği yön filtreleri. "in" varsayılan: bir ajan için "mesaj geldi mi"
30
- # sorusuna kendi gönderdiği mail cevap olamaz.
30
+ # Yön filtreleri (sunucu sözleşmesi). "in" varsayılan: bir ajan için "mesaj geldi mi"
31
+ # sorusuna kendi gönderdiği mail cevap olamaz. (Yorum — help() çıktısına girmez.)
31
32
  DIRECTIONS = ("in", "out", "all")
32
33
 
33
34
 
@@ -41,7 +42,8 @@ def _check_direction(direction: str) -> str:
41
42
  class Attachment:
42
43
  """Attachment metadata. The bytes are fetched on demand with `.download()`.
43
44
 
44
- İçerik listede GELMEZ: tek ekli 5 MB'lık bir mail, mesaj listesini kullanılamaz yapardı.
45
+ Contents are never included in a message listing: a single 5 MB attachment would make
46
+ the list unusable, so the bytes live behind a separate request.
45
47
  """
46
48
 
47
49
  id: int | None = None
@@ -104,8 +106,11 @@ class Message:
104
106
  # read this instead of parsing the body themselves.
105
107
  links: list[str] = field(default_factory=list)
106
108
  attachments: list[Attachment] = field(default_factory=list)
107
- # {score, required, is_spam, rules, scanner} — None means NOT SCANNED, which is not
108
- # the same as a score of 0.0.
109
+ # {score, required, is_spam, rules, scanner}, or None when the message was NOT SCANNED
110
+ # (not the same as a score of 0.0).
111
+ # NOTE: spam scanning is currently disabled on mailflat.net, so in practice this is
112
+ # None for every message today. The field is wired end to end and will start carrying
113
+ # data as soon as scanning is switched back on.
109
114
  spam: dict[str, Any] | None = None
110
115
  # Raw message headers (Message-ID lives here — needed for threading and dedup).
111
116
  # None on encrypted inboxes: headers carry the Subject, so they go inside the envelope.
@@ -123,6 +128,26 @@ class Message:
123
128
  )
124
129
  return self._inbox.delete_message(self.id)
125
130
 
131
+ def header(self, name: str, default: Any = None) -> Any:
132
+ """Look up a header case-insensitively, e.g. `msg.header("message-id")`.
133
+
134
+ Header names are case-insensitive per RFC 5322, but `headers` is a plain dict whose
135
+ keys come straight off the wire — the real key is `Message-ID`, so
136
+ `headers["Message-Id"]` raises KeyError. Use this instead of indexing.
137
+ """
138
+ if not self.headers:
139
+ return default
140
+ target = name.replace("_", "-").lower()
141
+ for key, value in self.headers.items():
142
+ if str(key).lower() == target:
143
+ return value
144
+ return default
145
+
146
+ @property
147
+ def message_id(self) -> str | None:
148
+ """This message's `Message-ID`, or None when headers were not stored."""
149
+ return self.header("message-id")
150
+
126
151
  def mark_read(self) -> dict[str, Any]:
127
152
  """Mark this message as read, so the next poll can skip it."""
128
153
  if self._inbox is None or self.id is None:
@@ -302,14 +327,14 @@ class Inbox:
302
327
  tracking state on its own.
303
328
  """
304
329
  return self._client._post(
305
- f"/api/v1/inboxes/{self.address}/messages/{message_id}/read")
330
+ f"/api/v1/inboxes/{self.address}/messages/{message_id}/read", idempotent=True)
306
331
 
307
332
  def burn(self) -> dict[str, Any]:
308
333
  """Delete every message in this inbox and keep the address.
309
334
 
310
335
  Useful between test scenarios: the address stays registered wherever you used it.
311
336
  """
312
- return self._client._post(f"/api/v1/inboxes/{self.address}/burn")
337
+ return self._client._post(f"/api/v1/inboxes/{self.address}/burn", idempotent=True)
313
338
 
314
339
  def delete(self) -> dict[str, Any]:
315
340
  """Delete this inbox and all of its messages. Cannot be undone."""
@@ -3,17 +3,15 @@
3
3
  A thin LangChain shell over the `mailflat` SDK; all HTTP and behaviour live in the client.
4
4
  `langchain-core` is an optional dependency → `pip install mailflat[langchain]`.
5
5
 
6
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
7
-
8
6
  Usage:
9
7
  from mailflat.langchain import MailFlatToolkit
10
8
  toolkit = MailFlatToolkit(api_key=env("MAILFLAT_KEY"))
11
9
  tools = toolkit.get_tools() # create_inbox, list_inboxes, read_messages, wait_for_otp,
12
10
  # wait_for_message, send_email, mark_read, burn_inbox,
13
- # delete_inbox (delete_message bilerek YOK)
11
+ # delete_inbox (delete_message is deliberately absent)
14
12
 
15
- ⚠️ Araç çıktıları `redact_secrets()`'ten geçer: per-inbox `api_key` modelin bağlamına
16
- (ve oradan LangSmith trace'lerine) girmemeli — bkz. B-055.
13
+ ⚠️ Tool output goes through `redact_secrets()`: a per-inbox `api_key` must never reach the
14
+ model's context (and from there LangSmith traces) — see B-055.
17
15
 
18
16
  Connected to:
19
17
  - imports from: mailflat.client (MailFlat), mailflat.redact, langchain_core.tools
@@ -22,6 +20,9 @@ Connected to:
22
20
  Key exports:
23
21
  - `MailFlatToolkit(api_key=..., base_url=...)` — `.get_tools()` → list[BaseTool]
24
22
  """
23
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
24
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
25
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
25
26
  from __future__ import annotations
26
27
 
27
28
  from typing import TYPE_CHECKING, Any
@@ -1,31 +1,32 @@
1
1
  """Strips secret fields from agent tool output (B-055).
2
2
 
3
- Neden var: `create_inbox` ve `list_inboxes` backend yanıtını olduğu gibi döndürüyordu ve o
4
- yanıtta per-inbox `api_key` (`mf_sk_…`) vardı. Araç çıktısı **model bağlamına** giriyor;
5
- oradan prompt loglarına, trace araçlarına (LangSmith, AI SDK telemetry), hata raporlarına ve
6
- modelin sonraki cevaplarına geçiyor. `list_inboxes` tek çağrıda hesabın **tüm** inbox
7
- anahtarlarını döküyordu.
3
+ Why it exists: `create_inbox` and `list_inboxes` used to return the backend payload as-is,
4
+ and that payload carried a per-inbox `api_key` (`mf_sk_...`). Tool output goes into the
5
+ MODEL's context, and from there into prompt logs, tracing tools (LangSmith, AI SDK
6
+ telemetry), error reports and the model's own later answers. `list_inboxes` leaked every
7
+ inbox key on the account in a single call.
8
8
 
9
- Ayrım bilinçli:
10
- - **SDK = kod yüzeyi.** `Inbox.api_key` duruyor; insanın yazdığı kod onu kullanabilir.
11
- - **Tool = model yüzeyi.** Buradan geçen her şey redakte edilir.
9
+ The split is deliberate:
10
+ - **SDK = code surface.** `Inbox.api_key` stays; code a human wrote may use it.
11
+ - **Tool = model surface.** Everything passing through here is redacted.
12
12
 
13
- Yöntem **isim bazlı**, değer bazlı değil: mesaj gövdesini tarayıp `mf_` ile başlayan
14
- string'leri silmek, kullanıcının kendi test anahtarını konu ettiği bir maili bozardı. Alan
15
- adına bakmak hem kesin hem zararsız.
16
-
17
- NOT: Bu dosya YAYIN yüzeyi (`packages/`) → docstring'ler İngilizce (CLAUDE.md §3 istisnası).
13
+ Redaction is **name-based, not value-based**: scanning message bodies for strings starting
14
+ with `mf_` would corrupt a legitimate email that happens to discuss a test key. Matching on
15
+ the field name is both exact and harmless.
18
16
 
19
17
  Connected to:
20
18
  - imports from: —
21
- - imported by: mailflat.langchain, mailflat's own tests. AYRICA repo içinde
22
- `packages/mcp` (mailflat-mcp, ayrı PyPI paketi) buradan import eder — bu paketin
23
- kendi içinde MCP yoktur, o yüzden referans repo düzeyindedir.
19
+ - imported by: mailflat.langchain and this package's tests. Within the repo,
20
+ `packages/mcp` (mailflat-mcp, a separate PyPI package) also imports it — that package
21
+ is not bundled here, so the reference is repo-level.
24
22
 
25
23
  Key exports:
26
24
  - `redact_secrets(value)` — copy of the input with secret fields removed
27
25
  - `SECRET_NAME_PARTS` — which field names count as secrets
28
26
  """
27
+ # NOT: `packages/` YAYIN yüzeyi → docstring'ler İngilizce (CLAUDE.md §3).
28
+ # Bu satırın kendisi bilerek YORUM: aynı notu docstring'e koymak tam da kaçındığımız
29
+ # şeydi — `help()` docstring'i basar, yorumu basmaz.
29
30
  from __future__ import annotations
30
31
 
31
32
  from typing import Any
File without changes
File without changes
File without changes