temptf 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,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .eggs/
9
+
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ .venv/
17
+ venv/
18
+ ENV/
19
+ .env
temptf-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liaksol
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.
temptf-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.5
2
+ Name: temptf
3
+ Version: 0.1.0
4
+ Summary: Python client for the temp.tf disposable email API
5
+ Project-URL: Homepage, https://github.com/patrikdzejn88-ux/temptf
6
+ Project-URL: Repository, https://github.com/patrikdzejn88-ux/temptf
7
+ Project-URL: Issues, https://github.com/patrikdzejn88-ux/temptf/issues
8
+ Author: Liaksol
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api-client,disposable-email,inbox,temp-tf,tempmail
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.8
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: Topic :: Communications :: Email
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.8
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.3.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # temptf
32
+
33
+ Python client library for the [temp.tf](https://temp.tf/) disposable email API. Zero external dependencies.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install git+https://github.com/patrikdzejn88-ux/temptf.git
39
+ ```
40
+
41
+ Or install locally from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/patrikdzejn88-ux/temptf.git
45
+ cd temptf
46
+ pip install .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ### Creating Addresses by Provider
52
+
53
+ ```python
54
+ import temptf
55
+
56
+ # Random address across all providers
57
+ email = temptf.create_email()
58
+
59
+ # Provider-specific addresses
60
+ gmail_any = temptf.create_gmail() # @gmail.com (dot or plus)
61
+ gmail_dot = temptf.create_gmail_dot() # u.s.e.r@gmail.com
62
+ gmail_plus = temptf.create_gmail_plus() # user+tag@gmail.com
63
+ outlook = temptf.create_outlook() # user+tag@outlook.com
64
+ hotmail = temptf.create_hotmail() # user+tag@hotmail.com
65
+ edu = temptf.create_edu() # @high.edu.pl
66
+ ```
67
+
68
+ ### Reading and Streaming Messages
69
+
70
+ ```python
71
+ import temptf
72
+
73
+ email = temptf.create_outlook()
74
+ print("Listening on:", email)
75
+
76
+ # Fetch current inbox contents once
77
+ messages = temptf.get_messages(email)
78
+
79
+ # Block until the first matching message arrives
80
+ msg = temptf.wait_for_message(email, timeout=60.0)
81
+ print(msg.subject, msg.text)
82
+
83
+ # Stream incoming messages continuously
84
+ for msg in temptf.stream_messages(email, interval=5.0):
85
+ print(f"[{msg.date}] {msg.sender}: {msg.subject}")
86
+ print(msg.text)
87
+ ```
88
+
89
+ ### Using `TempTFClient` Directly
90
+
91
+ ```python
92
+ from temptf import TempTFClient
93
+
94
+ client = TempTFClient(timeout=10.0)
95
+ email = client.create_gmail_plus()
96
+
97
+ for msg in client.stream_messages(email):
98
+ for att in msg.attachments:
99
+ client.download_attachment(email, msg.id, att.id, dest=f"./downloads/{att.name}")
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
temptf-0.1.0/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # temptf
2
+
3
+ Python client library for the [temp.tf](https://temp.tf/) disposable email API. Zero external dependencies.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install git+https://github.com/patrikdzejn88-ux/temptf.git
9
+ ```
10
+
11
+ Or install locally from source:
12
+
13
+ ```bash
14
+ git clone https://github.com/patrikdzejn88-ux/temptf.git
15
+ cd temptf
16
+ pip install .
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Creating Addresses by Provider
22
+
23
+ ```python
24
+ import temptf
25
+
26
+ # Random address across all providers
27
+ email = temptf.create_email()
28
+
29
+ # Provider-specific addresses
30
+ gmail_any = temptf.create_gmail() # @gmail.com (dot or plus)
31
+ gmail_dot = temptf.create_gmail_dot() # u.s.e.r@gmail.com
32
+ gmail_plus = temptf.create_gmail_plus() # user+tag@gmail.com
33
+ outlook = temptf.create_outlook() # user+tag@outlook.com
34
+ hotmail = temptf.create_hotmail() # user+tag@hotmail.com
35
+ edu = temptf.create_edu() # @high.edu.pl
36
+ ```
37
+
38
+ ### Reading and Streaming Messages
39
+
40
+ ```python
41
+ import temptf
42
+
43
+ email = temptf.create_outlook()
44
+ print("Listening on:", email)
45
+
46
+ # Fetch current inbox contents once
47
+ messages = temptf.get_messages(email)
48
+
49
+ # Block until the first matching message arrives
50
+ msg = temptf.wait_for_message(email, timeout=60.0)
51
+ print(msg.subject, msg.text)
52
+
53
+ # Stream incoming messages continuously
54
+ for msg in temptf.stream_messages(email, interval=5.0):
55
+ print(f"[{msg.date}] {msg.sender}: {msg.subject}")
56
+ print(msg.text)
57
+ ```
58
+
59
+ ### Using `TempTFClient` Directly
60
+
61
+ ```python
62
+ from temptf import TempTFClient
63
+
64
+ client = TempTFClient(timeout=10.0)
65
+ email = client.create_gmail_plus()
66
+
67
+ for msg in client.stream_messages(email):
68
+ for att in msg.attachments:
69
+ client.download_attachment(email, msg.id, att.id, dest=f"./downloads/{att.name}")
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "temptf"
7
+ version = "0.1.0"
8
+ description = "Python client for the temp.tf disposable email API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "Liaksol" },
14
+ ]
15
+ keywords = ["tempmail", "disposable-email", "temp-tf", "inbox", "api-client"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Communications :: Email",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/patrikdzejn88-ux/temptf"
34
+ Repository = "https://github.com/patrikdzejn88-ux/temptf"
35
+ Issues = "https://github.com/patrikdzejn88-ux/temptf/issues"
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=7.0.0",
40
+ "ruff>=0.3.0",
41
+ "mypy>=1.0.0",
42
+ ]
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/temptf"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+ target-version = "py38"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "I", "B", "UP"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
@@ -0,0 +1,121 @@
1
+ from typing import Callable, Iterator, List, Optional, Sequence, Union
2
+
3
+ from .client import PROVIDERS, TempTFClient
4
+ from .exceptions import APIError, RateLimitError, TempTFError
5
+ from .models import Attachment, Message, Stats
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ _default_client: Optional[TempTFClient] = None
10
+
11
+
12
+ def _get_client() -> TempTFClient:
13
+ global _default_client
14
+ if _default_client is None:
15
+ _default_client = TempTFClient()
16
+ return _default_client
17
+
18
+
19
+ def create_email(
20
+ providers: Optional[Union[str, Sequence[str]]] = None,
21
+ dot: bool = True,
22
+ plus: bool = True,
23
+ ) -> str:
24
+ """Generate a random temporary email address."""
25
+ return _get_client().create_email(providers=providers, dot=dot, plus=plus)
26
+
27
+
28
+ def create_gmail(dot: bool = True, plus: bool = True) -> str:
29
+ """Generate a @gmail.com address using dot syntax, plus alias, or both."""
30
+ return _get_client().create_gmail(dot=dot, plus=plus)
31
+
32
+
33
+ def create_gmail_dot() -> str:
34
+ """Generate a @gmail.com dot-variant address."""
35
+ return _get_client().create_gmail_dot()
36
+
37
+
38
+ def create_gmail_plus() -> str:
39
+ """Generate a @gmail.com plus-alias address."""
40
+ return _get_client().create_gmail_plus()
41
+
42
+
43
+ def create_outlook() -> str:
44
+ """Generate an @outlook.com address."""
45
+ return _get_client().create_outlook()
46
+
47
+
48
+ def create_hotmail() -> str:
49
+ """Generate a @hotmail.com address."""
50
+ return _get_client().create_hotmail()
51
+
52
+
53
+ def create_edu() -> str:
54
+ """Generate a @high.edu.pl address."""
55
+ return _get_client().create_edu()
56
+
57
+
58
+ def get_messages(email: str) -> List[Message]:
59
+ """Fetch all messages in the inbox for the given address."""
60
+ return _get_client().get_messages(email)
61
+
62
+
63
+ def wait_for_message(
64
+ email: str,
65
+ timeout: float = 120.0,
66
+ interval: float = 4.0,
67
+ predicate: Optional[Callable[[Message], bool]] = None,
68
+ ) -> Message:
69
+ """Poll the inbox until a message arrives or timeout expires."""
70
+ return _get_client().wait_for_message(
71
+ email=email,
72
+ timeout=timeout,
73
+ interval=interval,
74
+ predicate=predicate,
75
+ )
76
+
77
+
78
+ def stream_messages(
79
+ email: str,
80
+ interval: float = 5.0,
81
+ skip_existing: bool = False,
82
+ ) -> Iterator[Message]:
83
+ """Yield new messages for the given address as they arrive."""
84
+ return _get_client().stream_messages(
85
+ email=email,
86
+ interval=interval,
87
+ skip_existing=skip_existing,
88
+ )
89
+
90
+
91
+ def get_stats(
92
+ providers: Optional[Union[str, Sequence[str]]] = None,
93
+ dot: bool = True,
94
+ plus: bool = True,
95
+ ) -> Stats:
96
+ """Return pool availability and message statistics."""
97
+ return _get_client().get_stats(providers=providers, dot=dot, plus=plus)
98
+
99
+
100
+ __all__ = [
101
+ "__version__",
102
+ "PROVIDERS",
103
+ "TempTFClient",
104
+ "TempTFError",
105
+ "APIError",
106
+ "RateLimitError",
107
+ "Message",
108
+ "Attachment",
109
+ "Stats",
110
+ "create_email",
111
+ "create_gmail",
112
+ "create_gmail_dot",
113
+ "create_gmail_plus",
114
+ "create_outlook",
115
+ "create_hotmail",
116
+ "create_edu",
117
+ "get_messages",
118
+ "wait_for_message",
119
+ "stream_messages",
120
+ "get_stats",
121
+ ]
@@ -0,0 +1,225 @@
1
+ import json
2
+ import time
3
+ import urllib.error
4
+ import urllib.parse
5
+ import urllib.request
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
8
+
9
+ from .exceptions import APIError, RateLimitError, TempTFError
10
+ from .models import Message, Stats
11
+
12
+ BASE_URL = "https://temp.tf/api"
13
+ PROVIDERS = ("gmail", "outlook", "hotmail", "high.edu.pl")
14
+
15
+
16
+ class TempTFClient:
17
+ """Client for the temp.tf disposable email API."""
18
+
19
+ def __init__(
20
+ self,
21
+ base_url: str = BASE_URL,
22
+ timeout: float = 15.0,
23
+ auto_retry_rate_limit: bool = True,
24
+ user_agent: str = "temptf-python/0.1.0",
25
+ opener: Optional[urllib.request.OpenerDirector] = None,
26
+ ):
27
+ self.base_url = base_url.rstrip("/")
28
+ self.timeout = timeout
29
+ self.auto_retry_rate_limit = auto_retry_rate_limit
30
+ self.user_agent = user_agent
31
+ self._opener = opener or urllib.request.build_opener()
32
+
33
+ def _request_raw(
34
+ self,
35
+ method: str,
36
+ endpoint: str,
37
+ params: Optional[Dict[str, str]] = None,
38
+ json_body: Optional[Dict[str, Any]] = None,
39
+ ) -> Tuple[bytes, Dict[str, str]]:
40
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
41
+ if params:
42
+ query = urllib.parse.urlencode(params)
43
+ url = f"{url}?{query}"
44
+
45
+ headers = {
46
+ "Accept": "application/json",
47
+ "User-Agent": self.user_agent,
48
+ }
49
+ data: Optional[bytes] = None
50
+ if json_body is not None:
51
+ data = json.dumps(json_body).encode("utf-8")
52
+ headers["Content-Type"] = "application/json"
53
+
54
+ while True:
55
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
56
+ try:
57
+ with self._opener.open(req, timeout=self.timeout) as resp:
58
+ resp_headers = {k.lower(): v for k, v in resp.headers.items()}
59
+ return resp.read(), resp_headers
60
+ except urllib.error.HTTPError as exc:
61
+ status = exc.code
62
+ raw_err = exc.read()
63
+ if status == 429:
64
+ retry_header = exc.headers.get("Retry-After") if exc.headers else None
65
+ retry_after = int(retry_header) if retry_header and retry_header.isdigit() else 5
66
+ if self.auto_retry_rate_limit:
67
+ time.sleep(retry_after)
68
+ continue
69
+ raise RateLimitError(retry_after=retry_after) from exc
70
+
71
+ try:
72
+ parsed = json.loads(raw_err.decode("utf-8", errors="replace"))
73
+ err_msg = parsed.get("error", raw_err.decode("utf-8", errors="replace"))
74
+ except ValueError:
75
+ err_msg = raw_err.decode("utf-8", errors="replace")
76
+ raise APIError(status, str(err_msg)) from exc
77
+ except urllib.error.URLError as exc:
78
+ raise TempTFError(f"Network error: {exc.reason}") from exc
79
+
80
+ def _request_json(
81
+ self,
82
+ method: str,
83
+ endpoint: str,
84
+ params: Optional[Dict[str, str]] = None,
85
+ json_body: Optional[Dict[str, Any]] = None,
86
+ ) -> Dict[str, Any]:
87
+ raw, _ = self._request_raw(method, endpoint, params=params, json_body=json_body)
88
+ try:
89
+ return dict(json.loads(raw.decode("utf-8")))
90
+ except ValueError as exc:
91
+ raise TempTFError("Invalid JSON response from server") from exc
92
+
93
+ def create_email(
94
+ self,
95
+ providers: Optional[Union[str, Sequence[str]]] = None,
96
+ dot: bool = True,
97
+ plus: bool = True,
98
+ ) -> str:
99
+ """Generate a random temporary email address."""
100
+ if not dot and not plus:
101
+ raise ValueError("At least one of 'dot' or 'plus' must be True.")
102
+
103
+ params: Dict[str, str] = {}
104
+ if dot:
105
+ params["dot"] = "1"
106
+ if plus:
107
+ params["plus"] = "1"
108
+
109
+ if providers:
110
+ params["providers"] = (
111
+ providers if isinstance(providers, str) else ",".join(providers)
112
+ )
113
+
114
+ payload = self._request_json("GET", "/account", params=params)
115
+ email = payload.get("email")
116
+ if not email:
117
+ raise TempTFError("No email returned for the requested parameters.")
118
+ return str(email)
119
+
120
+ def create_gmail(self, dot: bool = True, plus: bool = True) -> str:
121
+ """Generate a @gmail.com address using dot syntax, plus alias, or both."""
122
+ return self.create_email(providers="gmail", dot=dot, plus=plus)
123
+
124
+ def create_gmail_dot(self) -> str:
125
+ """Generate a @gmail.com dot-variant address (e.g. u.s.e.r@gmail.com)."""
126
+ return self.create_email(providers="gmail", dot=True, plus=False)
127
+
128
+ def create_gmail_plus(self) -> str:
129
+ """Generate a @gmail.com plus-alias address (e.g. user+tag@gmail.com)."""
130
+ return self.create_email(providers="gmail", dot=False, plus=True)
131
+
132
+ def create_outlook(self) -> str:
133
+ """Generate an @outlook.com plus-alias address."""
134
+ return self.create_email(providers="outlook", dot=False, plus=True)
135
+
136
+ def create_hotmail(self) -> str:
137
+ """Generate a @hotmail.com plus-alias address."""
138
+ return self.create_email(providers="hotmail", dot=False, plus=True)
139
+
140
+ def create_edu(self) -> str:
141
+ """Generate a @high.edu.pl educational email address."""
142
+ return self.create_email(providers="high.edu.pl", dot=False, plus=True)
143
+
144
+ def get_messages(self, email: str) -> List[Message]:
145
+ """Fetch all messages currently in the inbox for the given address."""
146
+ payload = self._request_json("POST", "/check", json_body={"email": email})
147
+ items = payload.get("data") or []
148
+ return [Message.from_dict(item) for item in items]
149
+
150
+ def wait_for_message(
151
+ self,
152
+ email: str,
153
+ timeout: float = 120.0,
154
+ interval: float = 4.0,
155
+ predicate: Optional[Callable[[Message], bool]] = None,
156
+ ) -> Message:
157
+ """Poll the inbox until a matching message arrives or timeout is reached."""
158
+ deadline = time.monotonic() + timeout
159
+ while True:
160
+ for msg in self.get_messages(email):
161
+ if predicate is None or predicate(msg):
162
+ return msg
163
+ if time.monotonic() >= deadline:
164
+ raise TimeoutError(f"Timed out waiting for message on {email}")
165
+ time.sleep(interval)
166
+
167
+ def stream_messages(
168
+ self,
169
+ email: str,
170
+ interval: float = 5.0,
171
+ skip_existing: bool = False,
172
+ ) -> Iterator[Message]:
173
+ """Continuously poll the inbox and yield new messages as they arrive."""
174
+ seen: Set[str] = set()
175
+ if skip_existing:
176
+ for msg in self.get_messages(email):
177
+ seen.add(msg.id or f"{msg.date}:{msg.subject}")
178
+
179
+ while True:
180
+ messages = self.get_messages(email)
181
+ for msg in reversed(messages):
182
+ key = msg.id or f"{msg.date}:{msg.subject}"
183
+ if key not in seen:
184
+ seen.add(key)
185
+ yield msg
186
+ time.sleep(interval)
187
+
188
+ def download_attachment(
189
+ self,
190
+ email: str,
191
+ message_id: str,
192
+ attachment_id: str,
193
+ dest: Optional[Union[str, Path]] = None,
194
+ ) -> bytes:
195
+ """Download attachment content and optionally write it to disk."""
196
+ params = {
197
+ "email": email,
198
+ "messageId": message_id,
199
+ "attachmentId": attachment_id,
200
+ }
201
+ content, _ = self._request_raw("GET", "/attachment", params=params)
202
+ if dest is not None:
203
+ path = Path(dest)
204
+ path.parent.mkdir(parents=True, exist_ok=True)
205
+ path.write_bytes(content)
206
+ return content
207
+
208
+ def get_stats(
209
+ self,
210
+ providers: Optional[Union[str, Sequence[str]]] = None,
211
+ dot: bool = True,
212
+ plus: bool = True,
213
+ ) -> Stats:
214
+ """Return pool availability and message statistics."""
215
+ params: Dict[str, str] = {}
216
+ if dot:
217
+ params["dot"] = "1"
218
+ if plus:
219
+ params["plus"] = "1"
220
+ if providers:
221
+ params["providers"] = (
222
+ providers if isinstance(providers, str) else ",".join(providers)
223
+ )
224
+ payload = self._request_json("GET", "/stats", params=params)
225
+ return Stats.from_dict(payload)
@@ -0,0 +1,19 @@
1
+ class TempTFError(Exception):
2
+ """Base exception for temptf errors."""
3
+
4
+
5
+ class APIError(TempTFError):
6
+ """Raised when the temp.tf API returns an error status code."""
7
+
8
+ def __init__(self, status_code: int, message: str):
9
+ self.status_code = status_code
10
+ self.message = message
11
+ super().__init__(f"HTTP {status_code}: {message}")
12
+
13
+
14
+ class RateLimitError(APIError):
15
+ """Raised when rate limit (HTTP 429) is exceeded and auto-retry is disabled."""
16
+
17
+ def __init__(self, retry_after: int, message: str = "Rate limit exceeded"):
18
+ self.retry_after = retry_after
19
+ super().__init__(429, message)
@@ -0,0 +1,86 @@
1
+ import html
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List
5
+
6
+
7
+ def _strip_html(raw: str) -> str:
8
+ if not raw:
9
+ return ""
10
+ cleaned = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", raw, flags=re.DOTALL | re.IGNORECASE)
11
+ cleaned = re.sub(r"<br\s*/?>", "\n", cleaned, flags=re.IGNORECASE)
12
+ cleaned = re.sub(r"</(p|div|tr|li|h[1-6])>", "\n", cleaned, flags=re.IGNORECASE)
13
+ cleaned = re.sub(r"<[^>]+>", "", cleaned)
14
+ cleaned = html.unescape(cleaned)
15
+ return "\n".join(line.strip() for line in cleaned.splitlines() if line.strip())
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Attachment:
20
+ id: str
21
+ name: str
22
+ content_type: str
23
+ size: int
24
+
25
+ @classmethod
26
+ def from_dict(cls, data: Dict[str, Any]) -> "Attachment":
27
+ return cls(
28
+ id=str(data.get("id", "")),
29
+ name=str(data.get("name", "")),
30
+ content_type=str(data.get("contentType", "application/octet-stream")),
31
+ size=int(data.get("size", 0)),
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Message:
37
+ id: str
38
+ sender: str
39
+ subject: str
40
+ date: str
41
+ body: str
42
+ body_content_type: str
43
+ attachments: List[Attachment] = field(default_factory=list)
44
+ inline_cids: Dict[str, str] = field(default_factory=dict)
45
+
46
+ @property
47
+ def text(self) -> str:
48
+ """Plain-text representation of the message body."""
49
+ if self.body_content_type.lower() == "html":
50
+ return _strip_html(self.body)
51
+ return self.body.strip()
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: Dict[str, Any]) -> "Message":
55
+ attachments = [Attachment.from_dict(item) for item in (data.get("attachments") or [])]
56
+ return cls(
57
+ id=str(data.get("id", "")),
58
+ sender=str(data.get("from", "")),
59
+ subject=str(data.get("subject", "")),
60
+ date=str(data.get("date", "")),
61
+ body=str(data.get("body", "")),
62
+ body_content_type=str(data.get("bodyContentType", "text")),
63
+ attachments=attachments,
64
+ inline_cids=dict(data.get("inlineCids") or {}),
65
+ )
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class Stats:
70
+ total_addresses: int
71
+ total_formatted: str
72
+ total_received: int
73
+ breakdown: Dict[str, int]
74
+ breakdown_formatted: Dict[str, str]
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> "Stats":
78
+ raw_breakdown = data.get("breakdown") or {}
79
+ breakdown = {k: int(v) for k, v in raw_breakdown.items()}
80
+ return cls(
81
+ total_addresses=int(data.get("totalAddresses", 0)),
82
+ total_formatted=str(data.get("totalFormatted", "0")),
83
+ total_received=int(data.get("totalReceived", 0)),
84
+ breakdown=breakdown,
85
+ breakdown_formatted=dict(data.get("breakdownFormatted") or {}),
86
+ )
File without changes
@@ -0,0 +1,125 @@
1
+ import io
2
+ import json
3
+ import unittest
4
+ import urllib.error
5
+ from email.message import Message as HTTPHeaders
6
+ from unittest.mock import MagicMock
7
+
8
+ from temptf import TempTFClient
9
+ from temptf.exceptions import APIError, RateLimitError
10
+
11
+
12
+ def _mock_opener(payload=None, raw_bytes: bytes = b""):
13
+ opener = MagicMock()
14
+ resp = MagicMock()
15
+ if payload is not None:
16
+ raw_bytes = json.dumps(payload).encode("utf-8")
17
+ resp.read.return_value = raw_bytes
18
+ resp.headers = {"Content-Type": "application/json"}
19
+ resp.__enter__.return_value = resp
20
+ resp.__exit__.return_value = False
21
+ opener.open.return_value = resp
22
+ return opener
23
+
24
+
25
+ class TestTempTFClient(unittest.TestCase):
26
+ def test_provider_specific_creation(self) -> None:
27
+ opener = _mock_opener({"email": "u.s.e.r@gmail.com"})
28
+ client = TempTFClient(opener=opener)
29
+
30
+ self.assertEqual(client.create_gmail_dot(), "u.s.e.r@gmail.com")
31
+ req = opener.open.call_args[0][0]
32
+ self.assertEqual(req.full_url, "https://temp.tf/api/account?dot=1&providers=gmail")
33
+
34
+ opener = _mock_opener({"email": "user+123@gmail.com"})
35
+ client = TempTFClient(opener=opener)
36
+ self.assertEqual(client.create_gmail_plus(), "user+123@gmail.com")
37
+ req = opener.open.call_args[0][0]
38
+ self.assertEqual(req.full_url, "https://temp.tf/api/account?plus=1&providers=gmail")
39
+
40
+ opener = _mock_opener({"email": "user+123@outlook.com"})
41
+ client = TempTFClient(opener=opener)
42
+ self.assertEqual(client.create_outlook(), "user+123@outlook.com")
43
+ req = opener.open.call_args[0][0]
44
+ self.assertEqual(req.full_url, "https://temp.tf/api/account?plus=1&providers=outlook")
45
+
46
+ opener = _mock_opener({"email": "user+123@hotmail.com"})
47
+ client = TempTFClient(opener=opener)
48
+ self.assertEqual(client.create_hotmail(), "user+123@hotmail.com")
49
+ req = opener.open.call_args[0][0]
50
+ self.assertEqual(req.full_url, "https://temp.tf/api/account?plus=1&providers=hotmail")
51
+
52
+ opener = _mock_opener({"email": "abc@high.edu.pl"})
53
+ client = TempTFClient(opener=opener)
54
+ self.assertEqual(client.create_edu(), "abc@high.edu.pl")
55
+ req = opener.open.call_args[0][0]
56
+ self.assertEqual(req.full_url, "https://temp.tf/api/account?plus=1&providers=high.edu.pl")
57
+
58
+ def test_get_messages_and_html_stripping(self) -> None:
59
+ payload = {
60
+ "data": [
61
+ {
62
+ "id": "101",
63
+ "from": "noreply@service.com",
64
+ "subject": "Verification Code",
65
+ "date": "2026-11-06T12:00:00.000Z",
66
+ "body": "<html><body><p>Your code is <b>482910</b></p></body></html>",
67
+ "bodyContentType": "html",
68
+ "attachments": [
69
+ {
70
+ "id": "att_1",
71
+ "name": "receipt.pdf",
72
+ "contentType": "application/pdf",
73
+ "size": 512,
74
+ }
75
+ ],
76
+ }
77
+ ],
78
+ "totalReceived": 500,
79
+ }
80
+ opener = _mock_opener(payload)
81
+ client = TempTFClient(opener=opener)
82
+
83
+ messages = client.get_messages("user+123@gmail.com")
84
+ self.assertEqual(len(messages), 1)
85
+ msg = messages[0]
86
+ self.assertEqual(msg.id, "101")
87
+ self.assertEqual(msg.sender, "noreply@service.com")
88
+ self.assertEqual(msg.subject, "Verification Code")
89
+ self.assertEqual(msg.text, "Your code is 482910")
90
+ self.assertEqual(len(msg.attachments), 1)
91
+ self.assertEqual(msg.attachments[0].name, "receipt.pdf")
92
+
93
+ def test_error_handling(self) -> None:
94
+ opener = MagicMock()
95
+ client = TempTFClient(opener=opener, auto_retry_rate_limit=False)
96
+
97
+ headers = HTTPHeaders()
98
+ headers["Retry-After"] = "12"
99
+ err_429 = urllib.error.HTTPError(
100
+ url="https://temp.tf/api/check",
101
+ code=429,
102
+ msg="Too Many Requests",
103
+ hdrs=headers,
104
+ fp=io.BytesIO(b'{"error":"Rate limit"}'),
105
+ )
106
+ opener.open.side_effect = err_429
107
+ with self.assertRaises(RateLimitError) as exc_info:
108
+ client.get_messages("user+123@gmail.com")
109
+ self.assertEqual(exc_info.exception.retry_after, 12)
110
+
111
+ err_403 = urllib.error.HTTPError(
112
+ url="https://temp.tf/api/check",
113
+ code=403,
114
+ msg="Forbidden",
115
+ hdrs=HTTPHeaders(),
116
+ fp=io.BytesIO(b'{"error":"Forbidden address"}'),
117
+ )
118
+ opener.open.side_effect = err_403
119
+ with self.assertRaises(APIError) as api_exc:
120
+ client.get_messages("invalid@example.com")
121
+ self.assertEqual(api_exc.exception.status_code, 403)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ unittest.main()