tropmail 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.
- tropmail-1.0.0/.github/workflows/ci.yml +33 -0
- tropmail-1.0.0/.github/workflows/release.yml +61 -0
- tropmail-1.0.0/.gitignore +15 -0
- tropmail-1.0.0/LICENSE +21 -0
- tropmail-1.0.0/Makefile +31 -0
- tropmail-1.0.0/PKG-INFO +223 -0
- tropmail-1.0.0/README.md +192 -0
- tropmail-1.0.0/examples/README.md +21 -0
- tropmail-1.0.0/examples/async_inbox.py +52 -0
- tropmail-1.0.0/examples/quickstart.py +47 -0
- tropmail-1.0.0/examples/save_attachments.py +43 -0
- tropmail-1.0.0/pyproject.toml +64 -0
- tropmail-1.0.0/spec/openapi.json +1068 -0
- tropmail-1.0.0/src/tropmail/__init__.py +81 -0
- tropmail-1.0.0/src/tropmail/_auth.py +27 -0
- tropmail-1.0.0/src/tropmail/_errors.py +105 -0
- tropmail-1.0.0/src/tropmail/_http.py +278 -0
- tropmail-1.0.0/src/tropmail/_models.py +209 -0
- tropmail-1.0.0/src/tropmail/_rate_limit.py +41 -0
- tropmail-1.0.0/src/tropmail/_throttle.py +64 -0
- tropmail-1.0.0/src/tropmail/_version.py +1 -0
- tropmail-1.0.0/src/tropmail/attachments.py +185 -0
- tropmail-1.0.0/src/tropmail/client.py +158 -0
- tropmail-1.0.0/src/tropmail/emails.py +423 -0
- tropmail-1.0.0/src/tropmail/mailbox.py +84 -0
- tropmail-1.0.0/src/tropmail/py.typed +0 -0
- tropmail-1.0.0/tests/conftest.py +115 -0
- tropmail-1.0.0/tests/test_async.py +62 -0
- tropmail-1.0.0/tests/test_client.py +203 -0
- tropmail-1.0.0/tests/test_contract.py +119 -0
- tropmail-1.0.0/tests/test_errors.py +93 -0
- tropmail-1.0.0/tests/test_retry_and_pagination.py +148 -0
- tropmail-1.0.0/tests/test_throttle.py +44 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
cache: pip
|
|
29
|
+
cache-dependency-path: pyproject.toml
|
|
30
|
+
- run: pip install -e ".[dev]"
|
|
31
|
+
- run: ruff check src tests
|
|
32
|
+
- run: mypy
|
|
33
|
+
- run: pytest -q
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a tag like v1.2.3 (must match pyproject.toml version).
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
dry_run:
|
|
10
|
+
description: Build without publishing
|
|
11
|
+
type: boolean
|
|
12
|
+
default: true
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.12'
|
|
25
|
+
|
|
26
|
+
- name: Verify the tag matches the packaged version
|
|
27
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
28
|
+
run: |
|
|
29
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
30
|
+
packaged="$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)"
|
|
31
|
+
if [ "$tag" != "$packaged" ]; then
|
|
32
|
+
echo "::error::tag v$tag does not match pyproject version $packaged"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
- run: pip install -e ".[dev]" build
|
|
37
|
+
- run: ruff check src tests
|
|
38
|
+
- run: mypy
|
|
39
|
+
- run: pytest -q
|
|
40
|
+
- run: python -m build
|
|
41
|
+
|
|
42
|
+
- uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: python-dist
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
needs: build
|
|
49
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
url: https://pypi.org/p/tropmail
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: python-dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
tropmail-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TropMail
|
|
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.
|
tropmail-1.0.0/Makefile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.PHONY: install test lint typecheck contract build clean
|
|
2
|
+
|
|
3
|
+
VENV := .venv
|
|
4
|
+
PY := $(VENV)/bin/python
|
|
5
|
+
|
|
6
|
+
$(VENV):
|
|
7
|
+
python3 -m venv $(VENV)
|
|
8
|
+
$(PY) -m pip install --quiet --upgrade pip
|
|
9
|
+
$(PY) -m pip install --quiet -e ".[dev]"
|
|
10
|
+
|
|
11
|
+
install: $(VENV)
|
|
12
|
+
|
|
13
|
+
test: $(VENV)
|
|
14
|
+
$(PY) -m pytest -q
|
|
15
|
+
|
|
16
|
+
lint: $(VENV)
|
|
17
|
+
$(PY) -m ruff check src tests examples
|
|
18
|
+
|
|
19
|
+
typecheck: $(VENV)
|
|
20
|
+
$(PY) -m mypy
|
|
21
|
+
$(PY) -m mypy examples
|
|
22
|
+
|
|
23
|
+
contract: $(VENV)
|
|
24
|
+
$(PY) -m pytest -q tests/test_contract.py
|
|
25
|
+
|
|
26
|
+
build: $(VENV)
|
|
27
|
+
$(PY) -m pip install --quiet build
|
|
28
|
+
$(PY) -m build
|
|
29
|
+
|
|
30
|
+
clean:
|
|
31
|
+
rm -rf $(VENV) dist build .pytest_cache .mypy_cache .ruff_cache
|
tropmail-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tropmail
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the TropMail API
|
|
5
|
+
Project-URL: Homepage, https://tropmail.com
|
|
6
|
+
Project-URL: Documentation, https://docs.tropmail.com
|
|
7
|
+
Project-URL: Repository, https://github.com/tropmail/tropmail-python
|
|
8
|
+
Author-email: TropMail <support@tropmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,email,sdk,tropmail,webmail
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications :: Email
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx<1,>=0.27
|
|
24
|
+
Requires-Dist: msgspec<1,>=0.18
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy<2,>=1.13; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff<1,>=0.8; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# tropmail
|
|
33
|
+
|
|
34
|
+
Official Python SDK for the [TropMail API](https://api.tropmail.com).
|
|
35
|
+
|
|
36
|
+
Sync and async clients, fully typed, with automatic pagination, retries, and
|
|
37
|
+
client-side rate-limit pacing.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install tropmail
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.10+. The client reads `TROPMAIL_API_KEY` unless you pass the key.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tropmail import TropMail
|
|
49
|
+
|
|
50
|
+
with TropMail() as client: # reads TROPMAIL_API_KEY
|
|
51
|
+
mailbox = client.mailbox.get()
|
|
52
|
+
print(f"{mailbox.email}: {mailbox.opened_count} opened")
|
|
53
|
+
|
|
54
|
+
for email in client.emails.iterate(status="Open"):
|
|
55
|
+
print(email.timestamp, email.from_.address, email.subject)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pass the key explicitly if you prefer:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
client = TropMail("your32charalphanumericapikeyhere")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Async
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import asyncio
|
|
68
|
+
from tropmail import AsyncTropMail
|
|
69
|
+
|
|
70
|
+
async def main() -> None:
|
|
71
|
+
async with AsyncTropMail() as client:
|
|
72
|
+
async for email in client.emails.iterate(status="Favorite"):
|
|
73
|
+
print(email.subject)
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Reading an email
|
|
79
|
+
|
|
80
|
+
`get()` returns the HTML view by default. Pass the email object you already have
|
|
81
|
+
instead of a bare id and the SDK forwards its timestamp for a faster lookup.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
page = client.emails.list(limit=10)
|
|
85
|
+
email = page.emails[0]
|
|
86
|
+
|
|
87
|
+
detail = client.emails.get(email, view="text")
|
|
88
|
+
print(detail.content)
|
|
89
|
+
|
|
90
|
+
for attachment in detail.attachments:
|
|
91
|
+
print(attachment.filename, attachment.size, attachment.scan_status)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Markdown
|
|
95
|
+
|
|
96
|
+
The markdown view is generated on demand and the server can block for up to a
|
|
97
|
+
minute before answering `504`. `get_markdown()` handles that retry for you:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
detail = client.emails.get_markdown(email)
|
|
101
|
+
print(detail.content)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Actions
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client.emails.favorite(email)
|
|
108
|
+
client.emails.close(email)
|
|
109
|
+
client.emails.block(email) # also blocks the sender for future mail
|
|
110
|
+
client.emails.clear_action(email) # clears the action, unblocking the sender
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
All of them are shorthands for `update()`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
client.emails.update(email, email_state="Open", action_status="Favorite")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Attachments
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
info = client.attachments.get(attachment_id)
|
|
123
|
+
print(info.filename, info.scan_status)
|
|
124
|
+
|
|
125
|
+
client.attachments.scan(attachment_id)
|
|
126
|
+
path = client.attachments.download_to(attachment_id, "/tmp/invoice.pdf")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The download URL lives on a separate host, so no API credentials are ever sent to it.
|
|
130
|
+
|
|
131
|
+
## Search
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
page = client.emails.search("invoice", limit=25)
|
|
135
|
+
for email in client.emails.search_iterate("invoice"):
|
|
136
|
+
print(email.subject)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Note that the API always reports `total: 0` for search results. The iterators
|
|
140
|
+
page until they see a short page rather than trusting `total`.
|
|
141
|
+
|
|
142
|
+
## Errors
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from tropmail import (
|
|
146
|
+
AuthenticationError, NotFoundError, RateLimitError,
|
|
147
|
+
TierError, TropMailError,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
client.emails.get("does-not-exist")
|
|
152
|
+
except NotFoundError as exc:
|
|
153
|
+
print(exc.status, exc.request_id)
|
|
154
|
+
except RateLimitError as exc:
|
|
155
|
+
print("retry after", exc.retry_after)
|
|
156
|
+
except TropMailError as exc:
|
|
157
|
+
print("request failed:", exc)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Every error carries `status`, `message`, and the `request_id` echoed by the API,
|
|
161
|
+
which is what support needs to trace a call.
|
|
162
|
+
|
|
163
|
+
| Exception | HTTP |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `ValidationError` | 400 |
|
|
166
|
+
| `AuthenticationError` | 401 |
|
|
167
|
+
| `TierError` | 403 |
|
|
168
|
+
| `NotFoundError` | 404 |
|
|
169
|
+
| `RateLimitError` | 429 |
|
|
170
|
+
| `MarkdownTimeoutError` | 504 |
|
|
171
|
+
| `ServerError` | 5xx |
|
|
172
|
+
| `ConnectionError` | transport failure |
|
|
173
|
+
|
|
174
|
+
## Rate limits
|
|
175
|
+
|
|
176
|
+
Budgets are per mailbox, per second: Pro 3, Ultimate 10, Enterprise 50.
|
|
177
|
+
|
|
178
|
+
The client reads the limit from response headers and paces itself with a token
|
|
179
|
+
bucket so you stay under the budget instead of collecting `429`s. Inspect the
|
|
180
|
+
current window at any time:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
print(client.rate_limit) # RateLimitInfo(limit=3, remaining=2, reset=1767225600)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Disable pacing with `TropMail(throttle=False)` if you manage concurrency yourself.
|
|
187
|
+
|
|
188
|
+
## Configuration
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
client = TropMail(
|
|
192
|
+
api_key=None, # else TROPMAIL_API_KEY
|
|
193
|
+
base_url="https://api.tropmail.com/api/v1",
|
|
194
|
+
timeout=120.0, # markdown can block ~60s
|
|
195
|
+
max_retries=3,
|
|
196
|
+
throttle=True,
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Retries use exponential backoff with full jitter on 429, 502, 503, 504, and
|
|
201
|
+
transport errors. Reads retry automatically; mutations never do.
|
|
202
|
+
|
|
203
|
+
## Examples
|
|
204
|
+
|
|
205
|
+
Runnable scripts live in [`examples/`](examples/): a quickstart, a concurrent
|
|
206
|
+
async inbox reader, and attachment downloading.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
make install
|
|
212
|
+
make test
|
|
213
|
+
make lint
|
|
214
|
+
make typecheck
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Docs
|
|
218
|
+
|
|
219
|
+
Guides and API reference: [docs.tropmail.com](https://docs.tropmail.com/sdks/python/).
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
tropmail-1.0.0/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# tropmail
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [TropMail API](https://api.tropmail.com).
|
|
4
|
+
|
|
5
|
+
Sync and async clients, fully typed, with automatic pagination, retries, and
|
|
6
|
+
client-side rate-limit pacing.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install tropmail
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.10+. The client reads `TROPMAIL_API_KEY` unless you pass the key.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from tropmail import TropMail
|
|
18
|
+
|
|
19
|
+
with TropMail() as client: # reads TROPMAIL_API_KEY
|
|
20
|
+
mailbox = client.mailbox.get()
|
|
21
|
+
print(f"{mailbox.email}: {mailbox.opened_count} opened")
|
|
22
|
+
|
|
23
|
+
for email in client.emails.iterate(status="Open"):
|
|
24
|
+
print(email.timestamp, email.from_.address, email.subject)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pass the key explicitly if you prefer:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
client = TropMail("your32charalphanumericapikeyhere")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Async
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from tropmail import AsyncTropMail
|
|
38
|
+
|
|
39
|
+
async def main() -> None:
|
|
40
|
+
async with AsyncTropMail() as client:
|
|
41
|
+
async for email in client.emails.iterate(status="Favorite"):
|
|
42
|
+
print(email.subject)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Reading an email
|
|
48
|
+
|
|
49
|
+
`get()` returns the HTML view by default. Pass the email object you already have
|
|
50
|
+
instead of a bare id and the SDK forwards its timestamp for a faster lookup.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
page = client.emails.list(limit=10)
|
|
54
|
+
email = page.emails[0]
|
|
55
|
+
|
|
56
|
+
detail = client.emails.get(email, view="text")
|
|
57
|
+
print(detail.content)
|
|
58
|
+
|
|
59
|
+
for attachment in detail.attachments:
|
|
60
|
+
print(attachment.filename, attachment.size, attachment.scan_status)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Markdown
|
|
64
|
+
|
|
65
|
+
The markdown view is generated on demand and the server can block for up to a
|
|
66
|
+
minute before answering `504`. `get_markdown()` handles that retry for you:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
detail = client.emails.get_markdown(email)
|
|
70
|
+
print(detail.content)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Actions
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client.emails.favorite(email)
|
|
77
|
+
client.emails.close(email)
|
|
78
|
+
client.emails.block(email) # also blocks the sender for future mail
|
|
79
|
+
client.emails.clear_action(email) # clears the action, unblocking the sender
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
All of them are shorthands for `update()`:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
client.emails.update(email, email_state="Open", action_status="Favorite")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Attachments
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
info = client.attachments.get(attachment_id)
|
|
92
|
+
print(info.filename, info.scan_status)
|
|
93
|
+
|
|
94
|
+
client.attachments.scan(attachment_id)
|
|
95
|
+
path = client.attachments.download_to(attachment_id, "/tmp/invoice.pdf")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The download URL lives on a separate host, so no API credentials are ever sent to it.
|
|
99
|
+
|
|
100
|
+
## Search
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
page = client.emails.search("invoice", limit=25)
|
|
104
|
+
for email in client.emails.search_iterate("invoice"):
|
|
105
|
+
print(email.subject)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Note that the API always reports `total: 0` for search results. The iterators
|
|
109
|
+
page until they see a short page rather than trusting `total`.
|
|
110
|
+
|
|
111
|
+
## Errors
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from tropmail import (
|
|
115
|
+
AuthenticationError, NotFoundError, RateLimitError,
|
|
116
|
+
TierError, TropMailError,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
client.emails.get("does-not-exist")
|
|
121
|
+
except NotFoundError as exc:
|
|
122
|
+
print(exc.status, exc.request_id)
|
|
123
|
+
except RateLimitError as exc:
|
|
124
|
+
print("retry after", exc.retry_after)
|
|
125
|
+
except TropMailError as exc:
|
|
126
|
+
print("request failed:", exc)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Every error carries `status`, `message`, and the `request_id` echoed by the API,
|
|
130
|
+
which is what support needs to trace a call.
|
|
131
|
+
|
|
132
|
+
| Exception | HTTP |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `ValidationError` | 400 |
|
|
135
|
+
| `AuthenticationError` | 401 |
|
|
136
|
+
| `TierError` | 403 |
|
|
137
|
+
| `NotFoundError` | 404 |
|
|
138
|
+
| `RateLimitError` | 429 |
|
|
139
|
+
| `MarkdownTimeoutError` | 504 |
|
|
140
|
+
| `ServerError` | 5xx |
|
|
141
|
+
| `ConnectionError` | transport failure |
|
|
142
|
+
|
|
143
|
+
## Rate limits
|
|
144
|
+
|
|
145
|
+
Budgets are per mailbox, per second: Pro 3, Ultimate 10, Enterprise 50.
|
|
146
|
+
|
|
147
|
+
The client reads the limit from response headers and paces itself with a token
|
|
148
|
+
bucket so you stay under the budget instead of collecting `429`s. Inspect the
|
|
149
|
+
current window at any time:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
print(client.rate_limit) # RateLimitInfo(limit=3, remaining=2, reset=1767225600)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Disable pacing with `TropMail(throttle=False)` if you manage concurrency yourself.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
client = TropMail(
|
|
161
|
+
api_key=None, # else TROPMAIL_API_KEY
|
|
162
|
+
base_url="https://api.tropmail.com/api/v1",
|
|
163
|
+
timeout=120.0, # markdown can block ~60s
|
|
164
|
+
max_retries=3,
|
|
165
|
+
throttle=True,
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Retries use exponential backoff with full jitter on 429, 502, 503, 504, and
|
|
170
|
+
transport errors. Reads retry automatically; mutations never do.
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
Runnable scripts live in [`examples/`](examples/): a quickstart, a concurrent
|
|
175
|
+
async inbox reader, and attachment downloading.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
make install
|
|
181
|
+
make test
|
|
182
|
+
make lint
|
|
183
|
+
make typecheck
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Docs
|
|
187
|
+
|
|
188
|
+
Guides and API reference: [docs.tropmail.com](https://docs.tropmail.com/sdks/python/).
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python examples
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pip install tropmail
|
|
5
|
+
export TROPMAIL_API_KEY=your32charalphanumericapikeyhere
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
| File | What it shows |
|
|
9
|
+
|---|---|
|
|
10
|
+
| [`quickstart.py`](quickstart.py) | mailbox summary, listing, reading one message, typed errors |
|
|
11
|
+
| [`async_inbox.py`](async_inbox.py) | `AsyncTropMail`, auto-paging `async for`, concurrent reads under the rate limit |
|
|
12
|
+
| [`save_attachments.py`](save_attachments.py) | scanning attachments and streaming them to disk |
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python examples/quickstart.py
|
|
16
|
+
python examples/async_inbox.py
|
|
17
|
+
python examples/save_attachments.py <email-id> ./downloads
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
These are type-checked in CI (`make lint typecheck`), so they cannot drift from
|
|
21
|
+
the SDK.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Fetch several message bodies concurrently with the async client.
|
|
2
|
+
|
|
3
|
+
export TROPMAIL_API_KEY=...
|
|
4
|
+
python examples/async_inbox.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from tropmail import AsyncTropMail, Email, EmailDetail, TropMailError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def read(client: AsyncTropMail, email: Email) -> EmailDetail | None:
|
|
15
|
+
"""Fetch one body, returning None instead of raising so one failure does
|
|
16
|
+
not sink the whole batch."""
|
|
17
|
+
try:
|
|
18
|
+
return await client.emails.get(email, view="text")
|
|
19
|
+
except TropMailError as error:
|
|
20
|
+
print(f"{email.id}: {error}")
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def main() -> None:
|
|
25
|
+
async with AsyncTropMail() as client:
|
|
26
|
+
mailbox = await client.mailbox.get()
|
|
27
|
+
print(f"{mailbox.email}\n")
|
|
28
|
+
|
|
29
|
+
# Collect the unread messages, capped so the example stays quick.
|
|
30
|
+
unread: list[Email] = []
|
|
31
|
+
async for email in client.emails.iterate(status="Open"):
|
|
32
|
+
unread.append(email)
|
|
33
|
+
if len(unread) == 10:
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
if not unread:
|
|
37
|
+
print("Nothing unread.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# The client's token bucket paces these against the tier's per-second
|
|
41
|
+
# budget, so firing them all at once will not produce 429s.
|
|
42
|
+
details = await asyncio.gather(*(read(client, email) for email in unread))
|
|
43
|
+
|
|
44
|
+
for email, detail in zip(unread, details, strict=True):
|
|
45
|
+
if detail is None:
|
|
46
|
+
continue
|
|
47
|
+
preview = " ".join(detail.content.split())[:80]
|
|
48
|
+
print(f"{email.from_.address:<24} {email.subject[:30]:<30} {preview}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Print a mailbox summary and read the newest message.
|
|
2
|
+
|
|
3
|
+
export TROPMAIL_API_KEY=...
|
|
4
|
+
python examples/quickstart.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from tropmail import NotFoundError, TropMail, TropMailError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
# No api_key argument: the client reads TROPMAIL_API_KEY and validates the
|
|
14
|
+
# format locally, so a malformed key fails before spending a round trip.
|
|
15
|
+
with TropMail() as client:
|
|
16
|
+
mailbox = client.mailbox.get()
|
|
17
|
+
print(
|
|
18
|
+
f"{mailbox.email} — {mailbox.opened_count} open, "
|
|
19
|
+
f"{mailbox.closed_count} closed, {mailbox.favorite_count} favorite\n"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
page = client.emails.list(limit=5)
|
|
23
|
+
if not page.emails:
|
|
24
|
+
print("The mailbox is empty.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
for email in page.emails:
|
|
28
|
+
print(f"{email.from_.address:<24} {email.subject[:40]:<40} {email.timestamp}")
|
|
29
|
+
|
|
30
|
+
# Passing the email object rather than its id forwards the timestamp
|
|
31
|
+
# for a faster lookup.
|
|
32
|
+
newest = page.emails[0]
|
|
33
|
+
try:
|
|
34
|
+
detail = client.emails.get(newest, view="text")
|
|
35
|
+
except NotFoundError:
|
|
36
|
+
print("\nThat message was deleted between listing and reading it.")
|
|
37
|
+
return
|
|
38
|
+
except TropMailError as error:
|
|
39
|
+
print(f"\nRead failed ({error.status}, request {error.request_id}): {error}")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
print(f"\n--- {detail.subject} ---")
|
|
43
|
+
print(detail.content[:500])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|