burnbox 1.0.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.
- burnbox-1.0.0/.github/workflows/ci.yml +42 -0
- burnbox-1.0.0/.gitignore +13 -0
- burnbox-1.0.0/LICENSE +21 -0
- burnbox-1.0.0/PKG-INFO +31 -0
- burnbox-1.0.0/README.md +240 -0
- burnbox-1.0.0/burnbox/__init__.py +34 -0
- burnbox-1.0.0/burnbox/__main__.py +4 -0
- burnbox-1.0.0/burnbox/api.py +181 -0
- burnbox-1.0.0/burnbox/cli.py +349 -0
- burnbox-1.0.0/burnbox/client.py +67 -0
- burnbox-1.0.0/burnbox/config.py +120 -0
- burnbox-1.0.0/burnbox/detectors/__init__.py +31 -0
- burnbox-1.0.0/burnbox/detectors/base.py +28 -0
- burnbox-1.0.0/burnbox/detectors/clipboard.py +80 -0
- burnbox-1.0.0/burnbox/detectors/engine.py +70 -0
- burnbox-1.0.0/burnbox/detectors/i18n.py +142 -0
- burnbox-1.0.0/burnbox/detectors/parsers/__init__.py +13 -0
- burnbox-1.0.0/burnbox/detectors/parsers/alphanumeric_otp.py +85 -0
- burnbox-1.0.0/burnbox/detectors/parsers/labeled_otp.py +51 -0
- burnbox-1.0.0/burnbox/detectors/parsers/numeric_otp.py +74 -0
- burnbox-1.0.0/burnbox/detectors/parsers/reset_link.py +53 -0
- burnbox-1.0.0/burnbox/detectors/parsers/url_code.py +51 -0
- burnbox-1.0.0/burnbox/exceptions.py +31 -0
- burnbox-1.0.0/burnbox/models.py +36 -0
- burnbox-1.0.0/burnbox/notifications.py +53 -0
- burnbox-1.0.0/burnbox/providers/__init__.py +3 -0
- burnbox-1.0.0/burnbox/providers/base.py +46 -0
- burnbox-1.0.0/burnbox/providers/guerrillamail.py +164 -0
- burnbox-1.0.0/burnbox/providers/mailgw.py +20 -0
- burnbox-1.0.0/burnbox/providers/mailtm.py +237 -0
- burnbox-1.0.0/burnbox/providers/onesecmail.py +138 -0
- burnbox-1.0.0/burnbox/providers/registry.py +84 -0
- burnbox-1.0.0/burnbox/providers/sanitize.py +14 -0
- burnbox-1.0.0/burnbox/providers/utils.py +25 -0
- burnbox-1.0.0/burnbox/retry.py +105 -0
- burnbox-1.0.0/burnbox/session.py +79 -0
- burnbox-1.0.0/pyproject.toml +59 -0
- burnbox-1.0.0/tests/__init__.py +0 -0
- burnbox-1.0.0/tests/conftest.py +0 -0
- burnbox-1.0.0/tests/test_api.py +128 -0
- burnbox-1.0.0/tests/test_cli.py +55 -0
- burnbox-1.0.0/tests/test_client.py +132 -0
- burnbox-1.0.0/tests/test_config.py +58 -0
- burnbox-1.0.0/tests/test_detectors.py +352 -0
- burnbox-1.0.0/tests/test_integration.py +113 -0
- burnbox-1.0.0/tests/test_models.py +23 -0
- burnbox-1.0.0/tests/test_notifications.py +41 -0
- burnbox-1.0.0/tests/test_providers.py +395 -0
- burnbox-1.0.0/tests/test_registry.py +115 -0
- burnbox-1.0.0/tests/test_retry.py +134 -0
- burnbox-1.0.0/tests/test_session.py +100 -0
- burnbox-1.0.0/uv.lock +597 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main, master]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v4
|
|
16
|
+
- run: uv sync --extra dev
|
|
17
|
+
- run: uv run ruff check .
|
|
18
|
+
- run: uv run mypy burnbox/
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v4
|
|
28
|
+
- run: uv sync --python ${{ matrix.python-version }} --extra dev
|
|
29
|
+
- run: uv run pytest -q
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
33
|
+
needs: [lint, test]
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: astral-sh/setup-uv@v4
|
|
41
|
+
- run: uv build
|
|
42
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
burnbox-1.0.0/.gitignore
ADDED
burnbox-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 burnbox contributors
|
|
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.
|
burnbox-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: burnbox
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Temporary email that burns after reading
|
|
5
|
+
Author: burnbox contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,disposable-email,otp,temp-mail,temporary-email
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Communications :: Email
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: html2text
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Requires-Dist: pyperclip
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: tomli; python_version < '3.11'
|
|
26
|
+
Requires-Dist: typer
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff; extra == 'dev'
|
burnbox-1.0.0/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# burnbox
|
|
2
|
+
|
|
3
|
+
**Temporary email that burns after reading.**
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
burnbox creates a disposable email address, watches for incoming messages, auto-detects OTP codes, copies them to your clipboard — then burns the account when you're done.
|
|
9
|
+
|
|
10
|
+
Requires Python >= 3.10.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install burnbox
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or with [pipx](https://pypa.github.io/pipx/) (recommended for CLI tools):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pipx install burnbox
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
1. **Register** — burnbox creates a temporary email account
|
|
27
|
+
2. **Poll** — watches for incoming messages every few seconds
|
|
28
|
+
3. **Detect** — finds OTP codes, verification links, and copies them to clipboard
|
|
29
|
+
4. **Burn** — deletes the account and session on exit (Ctrl+C)
|
|
30
|
+
|
|
31
|
+
The `--keep` flag preserves the account for later use with `burnbox resume`.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
$ burnbox
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's it. You'll get a temp address, it auto-copies to clipboard, and burnbox watches for messages. When a verification code arrives, it's detected and copied. Press Ctrl+C to exit — the account is deleted automatically.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
╭─────────── burnbox ───────────╮
|
|
43
|
+
│ Temp email that burns after │
|
|
44
|
+
│ reading │
|
|
45
|
+
╰───────────────────────────────╯
|
|
46
|
+
|
|
47
|
+
Provider: mailtm
|
|
48
|
+
Address: k7x9m2@example.com
|
|
49
|
+
Address copied to clipboard
|
|
50
|
+
|
|
51
|
+
Ctrl+C to exit and burn · --keep to preserve · burnbox resume
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## CLI usage
|
|
55
|
+
|
|
56
|
+
| Command | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `burnbox` | Create temp email, watch for messages, burn on exit |
|
|
59
|
+
| `burnbox address` | Generate a temp email address and exit (account is burned immediately) |
|
|
60
|
+
| `burnbox resume` | Reconnect to the last saved session |
|
|
61
|
+
|
|
62
|
+
### Options
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
--provider Provider: mailtm, mailgw, 1secmail, guerrillamail
|
|
66
|
+
--poll, -p Polling interval in seconds (default: 5)
|
|
67
|
+
--timeout, -t HTTP request timeout (default: 10)
|
|
68
|
+
--keep, -k Keep account alive after exit
|
|
69
|
+
--version, -v Show version
|
|
70
|
+
--help, -h Show help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Examples
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Use a specific provider
|
|
77
|
+
burnbox --provider guerrillamail
|
|
78
|
+
|
|
79
|
+
# Keep account alive for later resume
|
|
80
|
+
burnbox --keep
|
|
81
|
+
|
|
82
|
+
# Resume a kept session
|
|
83
|
+
burnbox resume
|
|
84
|
+
|
|
85
|
+
# One-shot: just get a temp address (burned immediately)
|
|
86
|
+
burnbox address
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Programmatic API
|
|
90
|
+
|
|
91
|
+
Use burnbox as a Python library:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
import burnbox
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
box = await burnbox.create()
|
|
99
|
+
async with box:
|
|
100
|
+
print(f"Address: {box.address}")
|
|
101
|
+
msg = await box.wait_for_message(timeout=60)
|
|
102
|
+
if msg:
|
|
103
|
+
print(f"Code: {msg.best_code}")
|
|
104
|
+
print(f"From: {msg.sender}")
|
|
105
|
+
# Account auto-burns on exit
|
|
106
|
+
|
|
107
|
+
asyncio.run(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### API reference
|
|
111
|
+
|
|
112
|
+
- `burnbox.create(provider=None, config=None)` — Create a `BurnBox` instance (await it first, then use as context manager)
|
|
113
|
+
- `box.address` — The temp email address
|
|
114
|
+
- `box.fetch_new()` — Fetch new messages
|
|
115
|
+
- `box.wait_for_message(timeout=60)` — Wait for the first message (returns `None` on timeout)
|
|
116
|
+
- `box.messages()` — Async iterator yielding messages as they arrive
|
|
117
|
+
- `box.burn()` — Delete the account manually
|
|
118
|
+
- `msg.id`, `msg.sender`, `msg.subject` — Message metadata
|
|
119
|
+
- `msg.content` — Message body as plain text
|
|
120
|
+
- `msg.best_code` — Highest-confidence OTP code detected (string or `None`)
|
|
121
|
+
- `msg.codes` — All detected codes as `CodeMatch` objects (`.value`, `.confidence`, `.kind`)
|
|
122
|
+
- `msg.links` — All detected links
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
Config file: `~/.config/burnbox.toml`
|
|
127
|
+
|
|
128
|
+
```toml
|
|
129
|
+
[provider]
|
|
130
|
+
default = "mailtm" # Preferred provider
|
|
131
|
+
custom_url = "https://..." # Custom API URL for mailtm/mailgw
|
|
132
|
+
|
|
133
|
+
[polling]
|
|
134
|
+
interval = 5.0 # Seconds between polls
|
|
135
|
+
timeout = 10.0 # HTTP timeout
|
|
136
|
+
|
|
137
|
+
[output]
|
|
138
|
+
copy_address = true # Copy address to clipboard
|
|
139
|
+
copy_code = true # Copy OTP codes to clipboard
|
|
140
|
+
notifications = true # Desktop notifications on OTP
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Environment variables override the config:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
BURNBOX_PROVIDER=guerrillamail
|
|
147
|
+
BURNBOX_POLL_INTERVAL=3
|
|
148
|
+
BURNBOX_TIMEOUT=15
|
|
149
|
+
BURNBOX_CUSTOM_URL=https://...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Providers
|
|
153
|
+
|
|
154
|
+
| Provider | Auth | Delete account | Domains | Custom URL |
|
|
155
|
+
|---|---|---|---|---|
|
|
156
|
+
| **mail.tm** | Register + token | Yes | Multiple | Yes |
|
|
157
|
+
| **mail.gw** | Register + token | Yes | Multiple | Yes |
|
|
158
|
+
| **1secmail** | None (stateless) | Auto-expire | Dynamic (via API) | No |
|
|
159
|
+
| **guerrillamail** | Session-based | Yes | sharklasers.com, grr.la, etc | No |
|
|
160
|
+
|
|
161
|
+
burnbox automatically selects the first available provider with a health check. If one is down, it falls back to the next.
|
|
162
|
+
|
|
163
|
+
## OTP Detection
|
|
164
|
+
|
|
165
|
+
burnbox detects verification codes from incoming emails using a multi-parser engine:
|
|
166
|
+
|
|
167
|
+
- **Labeled OTP** — "code: 1234", "Your verification code: 8472", "Ваш код: 5531", etc.
|
|
168
|
+
- **Alphanumeric codes** — Recovery codes, backup keys (e.g., `A1B2-C3D4-E5F6`)
|
|
169
|
+
- **URL-embedded codes** — `?code=`, `?token=`, `?otp=` in links
|
|
170
|
+
- **Reset/verify links** — Password reset, account verification URLs
|
|
171
|
+
- **Numeric OTP** — Standalone digit clusters with context-aware confidence boosting
|
|
172
|
+
|
|
173
|
+
Supports 12 languages for label detection: English, Russian, German, French, Spanish, Portuguese, Chinese, Japanese, Korean, Hindi, Arabic, Turkish. Context-aware confidence boosting is available for English and Russian; other languages use label-matching only.
|
|
174
|
+
|
|
175
|
+
Each match has a **confidence score** (0–1). The highest-confidence code is auto-copied to your clipboard.
|
|
176
|
+
|
|
177
|
+
## Plugin system
|
|
178
|
+
|
|
179
|
+
Add custom providers via Python entry points:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# my_provider.py
|
|
183
|
+
from burnbox.providers.base import Provider
|
|
184
|
+
from burnbox.models import Session, InboxMessage
|
|
185
|
+
|
|
186
|
+
class MyProvider:
|
|
187
|
+
name = "myprovider"
|
|
188
|
+
supports_custom_url = False
|
|
189
|
+
|
|
190
|
+
async def is_alive(self) -> bool:
|
|
191
|
+
# Check if the provider API is reachable
|
|
192
|
+
|
|
193
|
+
async def register(self) -> Session:
|
|
194
|
+
# Create a new temp email account, return Session
|
|
195
|
+
|
|
196
|
+
async def restore(self, session: Session) -> None:
|
|
197
|
+
# Restore auth state from a saved session
|
|
198
|
+
|
|
199
|
+
async def fetch_messages(self, seen_ids: set[str]) -> list[InboxMessage]:
|
|
200
|
+
# Fetch new (unseen) messages
|
|
201
|
+
|
|
202
|
+
async def delete_account(self, account_id: str) -> bool:
|
|
203
|
+
# Delete the account, return True on success
|
|
204
|
+
|
|
205
|
+
async def aclose(self) -> None:
|
|
206
|
+
# Close the HTTP client
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```toml
|
|
210
|
+
# pyproject.toml
|
|
211
|
+
[project.entry-points."burnbox.providers"]
|
|
212
|
+
myprovider = "my_provider:MyProvider"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
After `pip install`, burnbox discovers your provider automatically.
|
|
216
|
+
|
|
217
|
+
## Security considerations
|
|
218
|
+
|
|
219
|
+
- OTP codes transit through third-party email providers. Only use burnbox for non-sensitive verifications.
|
|
220
|
+
- Session files are stored with 0600 permissions at `~/.config/burnbox/session.json`.
|
|
221
|
+
- Accounts are deleted ("burned") on exit by default. Use `--keep` only if you need persistence.
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
- **Clipboard not working on Linux**: Install `xclip` or `xsel` (X11) or `wl-clipboard` (Wayland).
|
|
226
|
+
- **All providers down**: burnbox falls back to trying providers even if health checks fail. Check your network.
|
|
227
|
+
- **"Session expired" on resume**: The temp email account has expired. Start a new one with `burnbox`.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
uv sync --extra dev
|
|
233
|
+
uv run ruff check .
|
|
234
|
+
uv run mypy burnbox/
|
|
235
|
+
uv run pytest
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from burnbox.api import BurnBox, Message, create
|
|
2
|
+
from burnbox.client import BurnBoxClient
|
|
3
|
+
from burnbox.config import AppConfig, load_config
|
|
4
|
+
from burnbox.exceptions import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthExpiredError,
|
|
7
|
+
BurnBoxError,
|
|
8
|
+
NoDomainsError,
|
|
9
|
+
ProviderError,
|
|
10
|
+
SessionError,
|
|
11
|
+
TokenError,
|
|
12
|
+
)
|
|
13
|
+
from burnbox.models import InboxMessage, MessagePreview, Session
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BurnBox",
|
|
19
|
+
"BurnBoxClient",
|
|
20
|
+
"Message",
|
|
21
|
+
"AppConfig",
|
|
22
|
+
"load_config",
|
|
23
|
+
"create",
|
|
24
|
+
"BurnBoxError",
|
|
25
|
+
"Session",
|
|
26
|
+
"InboxMessage",
|
|
27
|
+
"MessagePreview",
|
|
28
|
+
"APIError",
|
|
29
|
+
"NoDomainsError",
|
|
30
|
+
"ProviderError",
|
|
31
|
+
"SessionError",
|
|
32
|
+
"TokenError",
|
|
33
|
+
"AuthExpiredError",
|
|
34
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from typing import AsyncIterator
|
|
6
|
+
|
|
7
|
+
from burnbox.client import BurnBoxClient
|
|
8
|
+
from burnbox.config import AppConfig, load_config
|
|
9
|
+
from burnbox.detectors.base import CodeMatch, MessageContext
|
|
10
|
+
from burnbox.detectors.engine import ParserEngine
|
|
11
|
+
from burnbox.exceptions import AuthExpiredError, BurnBoxError
|
|
12
|
+
from burnbox.models import InboxMessage, Session
|
|
13
|
+
from burnbox.providers.base import Provider
|
|
14
|
+
from burnbox.providers.registry import select_provider
|
|
15
|
+
from burnbox.providers.utils import build_registry
|
|
16
|
+
from burnbox.session import SessionStore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _select(config: AppConfig) -> Provider:
|
|
20
|
+
registry = build_registry(config.custom_url)
|
|
21
|
+
all_providers = registry.all()
|
|
22
|
+
provider = await select_provider(all_providers, preferred=config.provider_default)
|
|
23
|
+
if not provider:
|
|
24
|
+
for p in all_providers:
|
|
25
|
+
try:
|
|
26
|
+
await p.aclose()
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
raise RuntimeError("No available providers. Check your network.")
|
|
30
|
+
for p in all_providers:
|
|
31
|
+
if p is not provider:
|
|
32
|
+
try:
|
|
33
|
+
await p.aclose()
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return provider
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Message:
|
|
40
|
+
def __init__(self, inner: InboxMessage, engine: ParserEngine) -> None:
|
|
41
|
+
self._inner = inner
|
|
42
|
+
self._engine = engine
|
|
43
|
+
self._codes: list[CodeMatch] | None = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def id(self) -> str:
|
|
47
|
+
return self._inner.id
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def sender(self) -> str:
|
|
51
|
+
return self._inner.sender
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def subject(self) -> str:
|
|
55
|
+
return self._inner.subject
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def content(self) -> str:
|
|
59
|
+
return self._inner.content
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def codes(self) -> list[CodeMatch]:
|
|
63
|
+
if self._codes is None:
|
|
64
|
+
ctx = MessageContext(sender=self.sender, subject=self.subject)
|
|
65
|
+
self._codes = self._engine.parse(self.content, ctx)
|
|
66
|
+
return self._codes
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def best_code(self) -> str | None:
|
|
70
|
+
best = self._engine.best_code(self.codes)
|
|
71
|
+
return best.value if best else None
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def links(self) -> list[str]:
|
|
75
|
+
return self._engine.detect_links(self.content)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BurnBox:
|
|
79
|
+
"""High-level async interface for temporary email.
|
|
80
|
+
|
|
81
|
+
Usage::
|
|
82
|
+
|
|
83
|
+
async with burnbox.create() as box:
|
|
84
|
+
print(box.address)
|
|
85
|
+
msg = await box.wait_for_message(timeout=60)
|
|
86
|
+
if msg:
|
|
87
|
+
print(msg.best_code)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
provider: Provider,
|
|
93
|
+
client: BurnBoxClient,
|
|
94
|
+
config: AppConfig,
|
|
95
|
+
engine: ParserEngine | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
self._provider = provider
|
|
98
|
+
self._client = client
|
|
99
|
+
self._config = config
|
|
100
|
+
self._engine = engine or ParserEngine()
|
|
101
|
+
self._seen_ids: set[str] = set()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def address(self) -> str | None:
|
|
105
|
+
s = self._client.session
|
|
106
|
+
return s.address if s else None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def session(self) -> Session | None:
|
|
110
|
+
return self._client.session
|
|
111
|
+
|
|
112
|
+
async def fetch_new(self) -> list[Message]:
|
|
113
|
+
raw = await self._client.fetch_new(self._seen_ids)
|
|
114
|
+
messages = [Message(m, self._engine) for m in raw]
|
|
115
|
+
for m in raw:
|
|
116
|
+
self._seen_ids.add(m.id)
|
|
117
|
+
return messages
|
|
118
|
+
|
|
119
|
+
async def wait_for_message(self, timeout: float = 60.0) -> Message | None:
|
|
120
|
+
loop = asyncio.get_running_loop()
|
|
121
|
+
deadline = loop.time() + timeout
|
|
122
|
+
while True:
|
|
123
|
+
messages = await self.fetch_new()
|
|
124
|
+
if messages:
|
|
125
|
+
return messages[0]
|
|
126
|
+
remaining = deadline - loop.time()
|
|
127
|
+
if remaining <= 0:
|
|
128
|
+
return None
|
|
129
|
+
await asyncio.sleep(min(self._config.poll_interval, remaining))
|
|
130
|
+
|
|
131
|
+
_MAX_CONSECUTIVE_ERRORS = 5
|
|
132
|
+
|
|
133
|
+
async def messages(self, poll_interval: float | None = None) -> AsyncIterator[Message]:
|
|
134
|
+
interval = poll_interval or self._config.poll_interval
|
|
135
|
+
consecutive_errors = 0
|
|
136
|
+
while True:
|
|
137
|
+
try:
|
|
138
|
+
new = await self.fetch_new()
|
|
139
|
+
consecutive_errors = 0
|
|
140
|
+
for m in new:
|
|
141
|
+
yield m
|
|
142
|
+
except (AuthExpiredError, BurnBoxError):
|
|
143
|
+
raise
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
consecutive_errors += 1
|
|
146
|
+
if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
|
|
147
|
+
raise BurnBoxError(
|
|
148
|
+
f"Too many consecutive errors ({consecutive_errors}). Last: {exc}"
|
|
149
|
+
) from exc
|
|
150
|
+
await asyncio.sleep(interval)
|
|
151
|
+
|
|
152
|
+
async def burn(self) -> bool:
|
|
153
|
+
return await self._client.burn()
|
|
154
|
+
|
|
155
|
+
async def __aenter__(self) -> BurnBox:
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
async def __aexit__(self, *args: object) -> None:
|
|
159
|
+
try:
|
|
160
|
+
await self.burn()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
try:
|
|
164
|
+
await self._provider.aclose()
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def create(
|
|
170
|
+
provider: str | None = None,
|
|
171
|
+
config: AppConfig | None = None,
|
|
172
|
+
) -> BurnBox:
|
|
173
|
+
cfg = config or load_config()
|
|
174
|
+
if provider:
|
|
175
|
+
cfg = replace(cfg, provider_default=provider)
|
|
176
|
+
|
|
177
|
+
prov = await _select(cfg)
|
|
178
|
+
store = SessionStore()
|
|
179
|
+
client = BurnBoxClient(provider=prov, session_store=store, config=cfg)
|
|
180
|
+
await client.register()
|
|
181
|
+
return BurnBox(provider=prov, client=client, config=cfg)
|