openemail 0.0.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.
- openemail-0.0.1/.gitignore +7 -0
- openemail-0.0.1/CHANGELOG.md +56 -0
- openemail-0.0.1/PKG-INFO +288 -0
- openemail-0.0.1/README.md +256 -0
- openemail-0.0.1/pyproject.toml +144 -0
- openemail-0.0.1/src/openemail/__init__.py +262 -0
- openemail-0.0.1/src/openemail/_async/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_async/client.py +383 -0
- openemail-0.0.1/src/openemail/_async/pagination.py +104 -0
- openemail-0.0.1/src/openemail/_async/resources/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_async/resources/account.py +226 -0
- openemail-0.0.1/src/openemail/_async/resources/addresses.py +89 -0
- openemail-0.0.1/src/openemail/_async/resources/analytics.py +88 -0
- openemail-0.0.1/src/openemail/_async/resources/app_host.py +72 -0
- openemail-0.0.1/src/openemail/_async/resources/audiences.py +374 -0
- openemail-0.0.1/src/openemail/_async/resources/billing.py +237 -0
- openemail-0.0.1/src/openemail/_async/resources/branding.py +85 -0
- openemail-0.0.1/src/openemail/_async/resources/broadcasts.py +315 -0
- openemail-0.0.1/src/openemail/_async/resources/calendar.py +215 -0
- openemail-0.0.1/src/openemail/_async/resources/chats.py +136 -0
- openemail-0.0.1/src/openemail/_async/resources/contacts.py +482 -0
- openemail-0.0.1/src/openemail/_async/resources/dns_connections.py +60 -0
- openemail-0.0.1/src/openemail/_async/resources/domains.py +613 -0
- openemail-0.0.1/src/openemail/_async/resources/drafts.py +153 -0
- openemail-0.0.1/src/openemail/_async/resources/emails.py +392 -0
- openemail-0.0.1/src/openemail/_async/resources/encryption.py +68 -0
- openemail-0.0.1/src/openemail/_async/resources/exports.py +84 -0
- openemail-0.0.1/src/openemail/_async/resources/files.py +345 -0
- openemail-0.0.1/src/openemail/_async/resources/forms.py +473 -0
- openemail-0.0.1/src/openemail/_async/resources/imports.py +284 -0
- openemail-0.0.1/src/openemail/_async/resources/keys.py +518 -0
- openemail-0.0.1/src/openemail/_async/resources/labels.py +154 -0
- openemail-0.0.1/src/openemail/_async/resources/languages.py +32 -0
- openemail-0.0.1/src/openemail/_async/resources/me.py +53 -0
- openemail-0.0.1/src/openemail/_async/resources/members.py +290 -0
- openemail-0.0.1/src/openemail/_async/resources/provider_imports.py +137 -0
- openemail-0.0.1/src/openemail/_async/resources/roles.py +156 -0
- openemail-0.0.1/src/openemail/_async/resources/rules.py +259 -0
- openemail-0.0.1/src/openemail/_async/resources/security.py +64 -0
- openemail-0.0.1/src/openemail/_async/resources/senders.py +45 -0
- openemail-0.0.1/src/openemail/_async/resources/settings.py +48 -0
- openemail-0.0.1/src/openemail/_async/resources/subscriptions.py +134 -0
- openemail-0.0.1/src/openemail/_async/resources/support.py +31 -0
- openemail-0.0.1/src/openemail/_async/resources/suppressions.py +141 -0
- openemail-0.0.1/src/openemail/_async/resources/temp_mail.py +230 -0
- openemail-0.0.1/src/openemail/_async/resources/templates.py +618 -0
- openemail-0.0.1/src/openemail/_async/resources/threads.py +400 -0
- openemail-0.0.1/src/openemail/_async/resources/tools.py +62 -0
- openemail-0.0.1/src/openemail/_async/resources/tracking.py +282 -0
- openemail-0.0.1/src/openemail/_async/resources/webhooks.py +559 -0
- openemail-0.0.1/src/openemail/_async/resources/workspaces.py +94 -0
- openemail-0.0.1/src/openemail/_async/transport.py +252 -0
- openemail-0.0.1/src/openemail/_build.py +1 -0
- openemail-0.0.1/src/openemail/_core/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_core/credentials.py +65 -0
- openemail-0.0.1/src/openemail/_core/environment.py +34 -0
- openemail-0.0.1/src/openemail/_core/errors.py +138 -0
- openemail-0.0.1/src/openemail/_core/hosts.py +234 -0
- openemail-0.0.1/src/openemail/_core/namespace.py +32 -0
- openemail-0.0.1/src/openemail/_core/request_path.py +39 -0
- openemail-0.0.1/src/openemail/_core/response.py +201 -0
- openemail-0.0.1/src/openemail/_core/retry.py +50 -0
- openemail-0.0.1/src/openemail/_core/sentinel.py +16 -0
- openemail-0.0.1/src/openemail/_core/update_notice.py +89 -0
- openemail-0.0.1/src/openemail/_core/wire.py +133 -0
- openemail-0.0.1/src/openemail/_default.py +76 -0
- openemail-0.0.1/src/openemail/_helpers/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_helpers/languages.py +59 -0
- openemail-0.0.1/src/openemail/_helpers/messages.py +13 -0
- openemail-0.0.1/src/openemail/_helpers/webhooks.py +115 -0
- openemail-0.0.1/src/openemail/_sync/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_sync/client.py +383 -0
- openemail-0.0.1/src/openemail/_sync/pagination.py +104 -0
- openemail-0.0.1/src/openemail/_sync/resources/__init__.py +0 -0
- openemail-0.0.1/src/openemail/_sync/resources/account.py +226 -0
- openemail-0.0.1/src/openemail/_sync/resources/addresses.py +87 -0
- openemail-0.0.1/src/openemail/_sync/resources/analytics.py +88 -0
- openemail-0.0.1/src/openemail/_sync/resources/app_host.py +72 -0
- openemail-0.0.1/src/openemail/_sync/resources/audiences.py +374 -0
- openemail-0.0.1/src/openemail/_sync/resources/billing.py +237 -0
- openemail-0.0.1/src/openemail/_sync/resources/branding.py +85 -0
- openemail-0.0.1/src/openemail/_sync/resources/broadcasts.py +315 -0
- openemail-0.0.1/src/openemail/_sync/resources/calendar.py +215 -0
- openemail-0.0.1/src/openemail/_sync/resources/chats.py +136 -0
- openemail-0.0.1/src/openemail/_sync/resources/contacts.py +482 -0
- openemail-0.0.1/src/openemail/_sync/resources/dns_connections.py +60 -0
- openemail-0.0.1/src/openemail/_sync/resources/domains.py +613 -0
- openemail-0.0.1/src/openemail/_sync/resources/drafts.py +153 -0
- openemail-0.0.1/src/openemail/_sync/resources/emails.py +392 -0
- openemail-0.0.1/src/openemail/_sync/resources/encryption.py +68 -0
- openemail-0.0.1/src/openemail/_sync/resources/exports.py +84 -0
- openemail-0.0.1/src/openemail/_sync/resources/files.py +345 -0
- openemail-0.0.1/src/openemail/_sync/resources/forms.py +473 -0
- openemail-0.0.1/src/openemail/_sync/resources/imports.py +284 -0
- openemail-0.0.1/src/openemail/_sync/resources/keys.py +518 -0
- openemail-0.0.1/src/openemail/_sync/resources/labels.py +154 -0
- openemail-0.0.1/src/openemail/_sync/resources/languages.py +32 -0
- openemail-0.0.1/src/openemail/_sync/resources/me.py +53 -0
- openemail-0.0.1/src/openemail/_sync/resources/members.py +290 -0
- openemail-0.0.1/src/openemail/_sync/resources/provider_imports.py +137 -0
- openemail-0.0.1/src/openemail/_sync/resources/roles.py +156 -0
- openemail-0.0.1/src/openemail/_sync/resources/rules.py +259 -0
- openemail-0.0.1/src/openemail/_sync/resources/security.py +64 -0
- openemail-0.0.1/src/openemail/_sync/resources/senders.py +45 -0
- openemail-0.0.1/src/openemail/_sync/resources/settings.py +48 -0
- openemail-0.0.1/src/openemail/_sync/resources/subscriptions.py +134 -0
- openemail-0.0.1/src/openemail/_sync/resources/support.py +31 -0
- openemail-0.0.1/src/openemail/_sync/resources/suppressions.py +141 -0
- openemail-0.0.1/src/openemail/_sync/resources/temp_mail.py +230 -0
- openemail-0.0.1/src/openemail/_sync/resources/templates.py +618 -0
- openemail-0.0.1/src/openemail/_sync/resources/threads.py +400 -0
- openemail-0.0.1/src/openemail/_sync/resources/tools.py +62 -0
- openemail-0.0.1/src/openemail/_sync/resources/tracking.py +282 -0
- openemail-0.0.1/src/openemail/_sync/resources/webhooks.py +559 -0
- openemail-0.0.1/src/openemail/_sync/resources/workspaces.py +94 -0
- openemail-0.0.1/src/openemail/_sync/transport.py +258 -0
- openemail-0.0.1/src/openemail/_version.py +1 -0
- openemail-0.0.1/src/openemail/constants/__init__.py +2565 -0
- openemail-0.0.1/src/openemail/constants/client.py +78 -0
- openemail-0.0.1/src/openemail/py.typed +0 -0
- openemail-0.0.1/src/openemail/types/__init__.py +1710 -0
- openemail-0.0.1/src/openemail/types/__init__.pyi +5865 -0
- openemail-0.0.1/src/openemail/types/client.py +25 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
The first release of the Python package. It covers the whole TypeScript SDK as it stood when this
|
|
6
|
+
release was cut, 0.0.8 plus the changes queued after it: 430 methods in 41 namespaces, each under the
|
|
7
|
+
TypeScript name in snake_case. `scripts/parity.py` proves that every one of them sends exactly the
|
|
8
|
+
request its TypeScript twin sends when given the same arguments, once with no options and once with
|
|
9
|
+
every option set.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
`OpenEmail` and `AsyncOpenEmail` have an attribute for every namespace, from `emails`, `threads` and
|
|
14
|
+
`templates` to `provider_imports`, `dns_connections` and `account`. `listAll` is `list_all`,
|
|
15
|
+
`getActive` is `get_active`, and `tempMail` is `temp_mail`. A request body is a dict whose keys keep
|
|
16
|
+
the API's camelCase names, the options of a call are snake_case keyword arguments
|
|
17
|
+
(`idempotency_key=`, `limit=`, and `from_=` where the option is called `from`), and a response comes
|
|
18
|
+
back as a dict. Every body and result has a `TypedDict` in `openemail.types`, named as in
|
|
19
|
+
TypeScript, so a type checker catches a misspelt key.
|
|
20
|
+
|
|
21
|
+
client = OpenEmail()
|
|
22
|
+
client.emails.send({'from': 'billing@acme.com', 'to': 'ada@example.com', 'subject': 'Invoice', 'text': 'Attached.'})
|
|
23
|
+
|
|
24
|
+
`AsyncOpenEmail` has the same methods with the same arguments, awaited, and runs on asyncio and on
|
|
25
|
+
trio.
|
|
26
|
+
|
|
27
|
+
Every paginated resource has `list`, which returns one page as
|
|
28
|
+
`{'items': [...], 'hasMore': ..., 'nextCursor': ...}`, `list_all`, which returns every item as a
|
|
29
|
+
list, and `iterate`, which yields each item and fetches the next page only when it gets there.
|
|
30
|
+
`addresses.list_all` is the exception and returns the whole address book.
|
|
31
|
+
|
|
32
|
+
An API refusal raises `OpenEmailApiError`, which carries `status`, `type`, `code`, `param`,
|
|
33
|
+
`doc_url`, `request_id`, `retry_after_seconds`, `fields` and `body`, and answers `is_auth`,
|
|
34
|
+
`is_permission`, `is_scope_missing`, `is_step_up_required`, `is_invalid_request`, `is_validation`,
|
|
35
|
+
`is_not_found`, `is_conflict`, `is_rate_limited`, `is_server_error` and `is_retryable`. No response
|
|
36
|
+
at all raises `OpenEmailNetworkError`, with `is_timeout` when the deadline passed. Both inherit
|
|
37
|
+
from `OpenEmailError`, and a mistake in the call itself raises `ValueError` before anything is sent.
|
|
38
|
+
|
|
39
|
+
`from openemail import openemail` is a ready-made client, built from `OPENEMAIL_API_KEY`,
|
|
40
|
+
`OPENEMAIL_ACCESS_TOKEN` and `OPENEMAIL_BASE_URL` the first time it is touched, and `init(...)`
|
|
41
|
+
configures it once at startup. `create_temp_mail()` and `create_async_temp_mail()` open disposable
|
|
42
|
+
inboxes with no credential.
|
|
43
|
+
|
|
44
|
+
`access_token=` takes an OAuth access token or a function that returns one, called before every
|
|
45
|
+
request, and on `AsyncOpenEmail` the function may be async. `verify_webhook_signature` checks a
|
|
46
|
+
delivery in constant time and returns the parsed event, or raises `WebhookVerificationError`.
|
|
47
|
+
`client.raw.request()` reaches an endpoint no method wraps yet, with the client's credential, base
|
|
48
|
+
URL, timeout and retries.
|
|
49
|
+
|
|
50
|
+
Reads are retried on 408 and 5xx, and a 429 only when it carries `Retry-After`, with any wait
|
|
51
|
+
longer than a minute raising instead of sleeping. `timeout` bounds the whole attempt, the response
|
|
52
|
+
body included, and uploads get ten minutes. A key, an access token or an inbox token is never sent
|
|
53
|
+
over plain HTTP, except to a server on this machine. Every request carries
|
|
54
|
+
`User-Agent: openemail-python/0.0.1`.
|
|
55
|
+
|
|
56
|
+
It needs Python 3.10 or newer, and depends only on `httpx`, `anyio` and `typing-extensions`.
|
openemail-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openemail
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The official Python SDK for the OpenEmail API. Send email and broadcasts, work with threads, drafts, labels, templates, rules, webhooks, tracking, contacts, audiences, sign-up forms, domains, suppressions, files, API keys, roles, members, mailbox imports and disposable inboxes from code.
|
|
5
|
+
Project-URL: Homepage, https://openemail.uk
|
|
6
|
+
Project-URL: Documentation, https://openemail.uk/docs/python
|
|
7
|
+
Project-URL: Reference, https://openemail.uk/docs/python/reference/methods
|
|
8
|
+
Project-URL: Changelog, https://openemail.uk/docs/python/changelog
|
|
9
|
+
Project-URL: Support, https://openemail.uk/contact
|
|
10
|
+
Author: OpenEmail
|
|
11
|
+
License-Expression: LicenseRef-Proprietary
|
|
12
|
+
Keywords: api,api client,email,email api,email templates,inbox,mailbox,openemail,sdk,send email,temp mail,transactional email,webhooks
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Communications :: Email
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: anyio>=3.7
|
|
29
|
+
Requires-Dist: httpx<1,>=0.27
|
|
30
|
+
Requires-Dist: typing-extensions>=4.12
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<div align='center'>
|
|
34
|
+
<a href='https://openemail.uk'>
|
|
35
|
+
<img
|
|
36
|
+
src='https://openemail.uk/logo.svg'
|
|
37
|
+
alt='OpenEmail Logo'
|
|
38
|
+
width='180'
|
|
39
|
+
/>
|
|
40
|
+
</a>
|
|
41
|
+
|
|
42
|
+
<br />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<p align='center'>
|
|
46
|
+
Email you can build on. Send mail, read the mailbox and automate a workspace from code.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p align='center'>
|
|
50
|
+
<a href='https://openemail.uk'>
|
|
51
|
+
<b>
|
|
52
|
+
Website
|
|
53
|
+
</b>
|
|
54
|
+
</a>
|
|
55
|
+
•
|
|
56
|
+
<a href='https://openemail.uk/docs/python'>
|
|
57
|
+
<b>
|
|
58
|
+
Documentation
|
|
59
|
+
</b>
|
|
60
|
+
</a>
|
|
61
|
+
•
|
|
62
|
+
<a href='https://openemail.uk/docs/python/reference/methods'>
|
|
63
|
+
<b>
|
|
64
|
+
Every method
|
|
65
|
+
</b>
|
|
66
|
+
</a>
|
|
67
|
+
•
|
|
68
|
+
<a href='https://openemail.uk/docs/api/reference'>
|
|
69
|
+
<b>
|
|
70
|
+
API Reference
|
|
71
|
+
</b>
|
|
72
|
+
</a>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
<br />
|
|
76
|
+
|
|
77
|
+
## Intro to the Python Package
|
|
78
|
+
|
|
79
|
+
The official Python client for the OpenEmail API. A method for every one of the 336 documented operations, 430 in all once the paging and upload helpers are counted, typed end to end. There is a synchronous client and an asynchronous one with the same methods, it runs on Python 3.10 and newer, and it depends only on `httpx`, `anyio` and `typing-extensions`.
|
|
80
|
+
|
|
81
|
+
It carries a workspace API key or an OAuth access token, so it belongs on a server or in a tool that runs on your own machine. The one exception is disposable inboxes, which need no credential.
|
|
82
|
+
|
|
83
|
+
### Installing
|
|
84
|
+
```bash
|
|
85
|
+
pip install openemail
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or `uv add openemail`, or `poetry add openemail`.
|
|
89
|
+
|
|
90
|
+
### Using
|
|
91
|
+
```python
|
|
92
|
+
from openemail import OpenEmail
|
|
93
|
+
|
|
94
|
+
client = OpenEmail('oe_live_...')
|
|
95
|
+
|
|
96
|
+
sent = client.emails.send({
|
|
97
|
+
'from': 'Acme Billing <billing@acme.com>',
|
|
98
|
+
'to': 'ada@example.com',
|
|
99
|
+
'subject': 'Your September invoice',
|
|
100
|
+
'html': '<p>Your invoice is attached.</p>',
|
|
101
|
+
'attachments': [{'filename': 'invoice.pdf', 'content': pdf_bytes, 'contentType': 'application/pdf'}],
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
print(sent['id'], sent['status'])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Request bodies are plain dictionaries with the API's own field names, so `'from'`, `'scheduledAt'` and `'replyTo'` read exactly as they do in the API reference. Responses are dictionaries too. Every body and response has a `TypedDict` in `openemail.types`, so your editor completes the keys and a type checker catches a misspelt one.
|
|
108
|
+
|
|
109
|
+
Create a key in OpenEmail under Settings, API keys. It is shown once, and it belongs in an environment variable rather than in code. `OpenEmail()` with no key reads `OPENEMAIL_API_KEY`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from openemail import OpenEmail
|
|
113
|
+
|
|
114
|
+
client = OpenEmail()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Build the client once, in a module of its own, and import it everywhere else. It keeps one connection pool, is safe to share between threads, and closes with `client.close()` or a `with` block.
|
|
118
|
+
|
|
119
|
+
Or skip even that. The package ships a ready made `openemail` client that reads `OPENEMAIL_API_KEY` the first time it is touched:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from openemail import openemail
|
|
123
|
+
|
|
124
|
+
openemail.emails.send({'from': sender, 'to': recipient, 'subject': subject, 'text': text})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Every send carries an idempotency key, generated once per call and reused by its retries, so a retried request replays the original message rather than sending a second one. Pass your own with `idempotency_key=` to make that hold across processes and restarts.
|
|
128
|
+
|
|
129
|
+
### Async
|
|
130
|
+
```python
|
|
131
|
+
import asyncio
|
|
132
|
+
|
|
133
|
+
from openemail import AsyncOpenEmail
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def main() -> None:
|
|
137
|
+
async with AsyncOpenEmail() as client:
|
|
138
|
+
sent = await client.emails.send({'from': sender, 'to': recipient, 'subject': 'Hi', 'text': 'Hello'})
|
|
139
|
+
|
|
140
|
+
async for thread in client.threads.iterate(folder='inbox'):
|
|
141
|
+
print(thread['id'])
|
|
142
|
+
|
|
143
|
+
print(sent['status'])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
asyncio.run(main())
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`AsyncOpenEmail` has every method `OpenEmail` has, with the same arguments, and runs on asyncio and trio.
|
|
150
|
+
|
|
151
|
+
### Reading the mailbox
|
|
152
|
+
```python
|
|
153
|
+
page = client.threads.list(folder='inbox', limit=25)
|
|
154
|
+
|
|
155
|
+
for thread in client.threads.iterate(folder='inbox', query='invoice'):
|
|
156
|
+
full = client.threads.get(thread['id'])
|
|
157
|
+
|
|
158
|
+
print(full['messageCount'], full['hasUnread'])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Every paginated resource has `list` for one page, `list_all` for every page at once and `iterate` to stream items and stop whenever you like. A page is `{'items': [...], 'hasMore': ..., 'nextCursor': ...}`, and `list_all` returns one list, apart from `addresses.list_all`, which returns the whole address book.
|
|
162
|
+
|
|
163
|
+
### Errors
|
|
164
|
+
```python
|
|
165
|
+
from openemail import OpenEmailApiError, openemail
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
openemail.templates.send('order-shipped', {
|
|
169
|
+
'from': 'dispatch@acme.com',
|
|
170
|
+
'to': 'ada@example.com',
|
|
171
|
+
'props': {'orderId': 'AC-4192'},
|
|
172
|
+
})
|
|
173
|
+
except OpenEmailApiError as error:
|
|
174
|
+
if error.is_validation:
|
|
175
|
+
print(error.code, error.param, error.request_id)
|
|
176
|
+
|
|
177
|
+
raise
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
An API refusal is one class, `OpenEmailApiError`, with `status`, `type`, `code`, `param` and `request_id`, plus `is_validation`, `is_not_found`, `is_rate_limited` and friends to branch on. No response at all is `OpenEmailNetworkError`, with `is_timeout` when the deadline passed. Both inherit `OpenEmailError`. An argument the client can tell is wrong before anything is sent, such as a malformed key, raises `ValueError`.
|
|
181
|
+
|
|
182
|
+
### Webhooks
|
|
183
|
+
```python
|
|
184
|
+
import os
|
|
185
|
+
|
|
186
|
+
from fastapi import FastAPI, Request, Response
|
|
187
|
+
from openemail import verify_webhook_signature
|
|
188
|
+
|
|
189
|
+
app = FastAPI()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.post('/webhooks/openemail')
|
|
193
|
+
async def webhook(request: Request) -> Response:
|
|
194
|
+
event = verify_webhook_signature(
|
|
195
|
+
payload=await request.body(),
|
|
196
|
+
headers=request.headers,
|
|
197
|
+
secret=os.environ['OPENEMAIL_WEBHOOK_SECRET'],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
print(event['type'], event['data'])
|
|
201
|
+
|
|
202
|
+
return Response(status_code=204)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
It checks the HMAC in constant time and rejects a delivery more than five minutes old, then returns the parsed event, or raises `WebhookVerificationError`. Pass the raw body as bytes or text: re-serialising it changes the bytes and the signature will not match. The headers can come from any framework, since the lookup ignores case.
|
|
206
|
+
|
|
207
|
+
### Disposable inboxes
|
|
208
|
+
```python
|
|
209
|
+
from openemail import create_temp_mail
|
|
210
|
+
|
|
211
|
+
temp = create_temp_mail()
|
|
212
|
+
|
|
213
|
+
inbox = temp.create({'ttlMinutes': 60})
|
|
214
|
+
|
|
215
|
+
messages = temp.list_messages(inbox['id'], inbox_token=inbox['token'])
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`create` needs no credential and is the only call that returns the inbox token, so keep it.
|
|
219
|
+
|
|
220
|
+
### OAuth access tokens
|
|
221
|
+
An app a person connected to OpenEmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as `access_token`:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from openemail import OpenEmail
|
|
225
|
+
|
|
226
|
+
client = OpenEmail(access_token=session.fresh_access_token)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`access_token` takes the token itself, or a function that returns it. The function runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. On `AsyncOpenEmail` the function may also be `async`. Pass `api_key` or `access_token`, not both. `OpenEmail()` reads `OPENEMAIL_ACCESS_TOKEN` when you pass neither and `OPENEMAIL_API_KEY` is not set. `me.get()` answers `'object': 'oauth_token'` for a token, with the connected app's `clientId` and `expiresAt`, when the person's approval of the app runs out.
|
|
230
|
+
|
|
231
|
+
A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request fails with `is_step_up_required`. Ask for a code, check it, then replay the request:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from openemail import OpenEmailApiError
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
client.domains.delete(domain_id)
|
|
238
|
+
except OpenEmailApiError as error:
|
|
239
|
+
if not error.is_step_up_required:
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
challenge = client.security.begin_step_up()
|
|
243
|
+
|
|
244
|
+
if challenge['method'] == 'email':
|
|
245
|
+
prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: '
|
|
246
|
+
else:
|
|
247
|
+
prompt = 'Enter the code from your authenticator app, or a backup code: '
|
|
248
|
+
|
|
249
|
+
client.security.verify_step_up({'code': input(prompt)})
|
|
250
|
+
client.domains.delete(domain_id)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
An emailed code works for 10 minutes, and `begin_step_up({'resend': True})` sends a fresh one. Once a code is verified the app is not asked again for 60 minutes. `security.step_up_status()` says whether it is verified right now. API keys are never asked for a code.
|
|
254
|
+
|
|
255
|
+
### Configuring
|
|
256
|
+
Pass keyword arguments when the defaults are not right:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import os
|
|
260
|
+
|
|
261
|
+
import httpx
|
|
262
|
+
from openemail import OpenEmail
|
|
263
|
+
|
|
264
|
+
client = OpenEmail(
|
|
265
|
+
os.environ['OPENEMAIL_API_KEY'],
|
|
266
|
+
base_url='https://api.openemail.uk',
|
|
267
|
+
timeout=30,
|
|
268
|
+
max_retries=2,
|
|
269
|
+
http_client=httpx.Client(proxy='http://proxy.internal:3128', follow_redirects=True),
|
|
270
|
+
headers={'X-Team': 'billing'},
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The shipped `openemail` client takes the same arguments through `init(...)`, called once at startup.
|
|
275
|
+
|
|
276
|
+
`base_url` also comes from `OPENEMAIL_BASE_URL`. Use an `https:` origin: the client refuses to send an API key, an access token or an inbox token over plain `http:`, and raises before the request leaves, unless the server is on this machine at `localhost`, a `127.x.x.x` address or `::1`. A `base_url` on `0.0.0.0` raises when the client is built, since that is the address a server listens on: use `127.0.0.1` with the same port.
|
|
277
|
+
|
|
278
|
+
`timeout` is in seconds and bounds the whole attempt, the response body included, and `0` turns it off. Reads are retried on 408 and 5xx with backoff. A 429 is retried only when it carries a `Retry-After`, and any wait longer than a minute raises instead of sleeping. Writes that cannot safely repeat are not retried. Every method outside `temp_mail` takes `api_key=` and `timeout=`, so one process can serve several workspaces with one client. The `temp_mail` methods take `inbox_token=` instead of `api_key=`.
|
|
279
|
+
|
|
280
|
+
An endpoint no method wraps yet is one `client.raw.request()` away, with the client's credential, base URL, timeout and retry policy applied:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The path must begin with a single `/`. Anything else, such as `//host/x`, raises before a request is sent, and so does a path whose finished URL leaves the base URL's origin, so the credential it carries never reaches another host.
|
|
287
|
+
|
|
288
|
+
When a newer version is on PyPI the client says so once on a terminal. `OPENEMAIL_DISABLE_UPDATE_NOTICE=1` or `disable_update_notice=True` turns that off.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<div align='center'>
|
|
2
|
+
<a href='https://openemail.uk'>
|
|
3
|
+
<img
|
|
4
|
+
src='https://openemail.uk/logo.svg'
|
|
5
|
+
alt='OpenEmail Logo'
|
|
6
|
+
width='180'
|
|
7
|
+
/>
|
|
8
|
+
</a>
|
|
9
|
+
|
|
10
|
+
<br />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<p align='center'>
|
|
14
|
+
Email you can build on. Send mail, read the mailbox and automate a workspace from code.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align='center'>
|
|
18
|
+
<a href='https://openemail.uk'>
|
|
19
|
+
<b>
|
|
20
|
+
Website
|
|
21
|
+
</b>
|
|
22
|
+
</a>
|
|
23
|
+
•
|
|
24
|
+
<a href='https://openemail.uk/docs/python'>
|
|
25
|
+
<b>
|
|
26
|
+
Documentation
|
|
27
|
+
</b>
|
|
28
|
+
</a>
|
|
29
|
+
•
|
|
30
|
+
<a href='https://openemail.uk/docs/python/reference/methods'>
|
|
31
|
+
<b>
|
|
32
|
+
Every method
|
|
33
|
+
</b>
|
|
34
|
+
</a>
|
|
35
|
+
•
|
|
36
|
+
<a href='https://openemail.uk/docs/api/reference'>
|
|
37
|
+
<b>
|
|
38
|
+
API Reference
|
|
39
|
+
</b>
|
|
40
|
+
</a>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<br />
|
|
44
|
+
|
|
45
|
+
## Intro to the Python Package
|
|
46
|
+
|
|
47
|
+
The official Python client for the OpenEmail API. A method for every one of the 336 documented operations, 430 in all once the paging and upload helpers are counted, typed end to end. There is a synchronous client and an asynchronous one with the same methods, it runs on Python 3.10 and newer, and it depends only on `httpx`, `anyio` and `typing-extensions`.
|
|
48
|
+
|
|
49
|
+
It carries a workspace API key or an OAuth access token, so it belongs on a server or in a tool that runs on your own machine. The one exception is disposable inboxes, which need no credential.
|
|
50
|
+
|
|
51
|
+
### Installing
|
|
52
|
+
```bash
|
|
53
|
+
pip install openemail
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or `uv add openemail`, or `poetry add openemail`.
|
|
57
|
+
|
|
58
|
+
### Using
|
|
59
|
+
```python
|
|
60
|
+
from openemail import OpenEmail
|
|
61
|
+
|
|
62
|
+
client = OpenEmail('oe_live_...')
|
|
63
|
+
|
|
64
|
+
sent = client.emails.send({
|
|
65
|
+
'from': 'Acme Billing <billing@acme.com>',
|
|
66
|
+
'to': 'ada@example.com',
|
|
67
|
+
'subject': 'Your September invoice',
|
|
68
|
+
'html': '<p>Your invoice is attached.</p>',
|
|
69
|
+
'attachments': [{'filename': 'invoice.pdf', 'content': pdf_bytes, 'contentType': 'application/pdf'}],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
print(sent['id'], sent['status'])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Request bodies are plain dictionaries with the API's own field names, so `'from'`, `'scheduledAt'` and `'replyTo'` read exactly as they do in the API reference. Responses are dictionaries too. Every body and response has a `TypedDict` in `openemail.types`, so your editor completes the keys and a type checker catches a misspelt one.
|
|
76
|
+
|
|
77
|
+
Create a key in OpenEmail under Settings, API keys. It is shown once, and it belongs in an environment variable rather than in code. `OpenEmail()` with no key reads `OPENEMAIL_API_KEY`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from openemail import OpenEmail
|
|
81
|
+
|
|
82
|
+
client = OpenEmail()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Build the client once, in a module of its own, and import it everywhere else. It keeps one connection pool, is safe to share between threads, and closes with `client.close()` or a `with` block.
|
|
86
|
+
|
|
87
|
+
Or skip even that. The package ships a ready made `openemail` client that reads `OPENEMAIL_API_KEY` the first time it is touched:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from openemail import openemail
|
|
91
|
+
|
|
92
|
+
openemail.emails.send({'from': sender, 'to': recipient, 'subject': subject, 'text': text})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every send carries an idempotency key, generated once per call and reused by its retries, so a retried request replays the original message rather than sending a second one. Pass your own with `idempotency_key=` to make that hold across processes and restarts.
|
|
96
|
+
|
|
97
|
+
### Async
|
|
98
|
+
```python
|
|
99
|
+
import asyncio
|
|
100
|
+
|
|
101
|
+
from openemail import AsyncOpenEmail
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def main() -> None:
|
|
105
|
+
async with AsyncOpenEmail() as client:
|
|
106
|
+
sent = await client.emails.send({'from': sender, 'to': recipient, 'subject': 'Hi', 'text': 'Hello'})
|
|
107
|
+
|
|
108
|
+
async for thread in client.threads.iterate(folder='inbox'):
|
|
109
|
+
print(thread['id'])
|
|
110
|
+
|
|
111
|
+
print(sent['status'])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`AsyncOpenEmail` has every method `OpenEmail` has, with the same arguments, and runs on asyncio and trio.
|
|
118
|
+
|
|
119
|
+
### Reading the mailbox
|
|
120
|
+
```python
|
|
121
|
+
page = client.threads.list(folder='inbox', limit=25)
|
|
122
|
+
|
|
123
|
+
for thread in client.threads.iterate(folder='inbox', query='invoice'):
|
|
124
|
+
full = client.threads.get(thread['id'])
|
|
125
|
+
|
|
126
|
+
print(full['messageCount'], full['hasUnread'])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Every paginated resource has `list` for one page, `list_all` for every page at once and `iterate` to stream items and stop whenever you like. A page is `{'items': [...], 'hasMore': ..., 'nextCursor': ...}`, and `list_all` returns one list, apart from `addresses.list_all`, which returns the whole address book.
|
|
130
|
+
|
|
131
|
+
### Errors
|
|
132
|
+
```python
|
|
133
|
+
from openemail import OpenEmailApiError, openemail
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
openemail.templates.send('order-shipped', {
|
|
137
|
+
'from': 'dispatch@acme.com',
|
|
138
|
+
'to': 'ada@example.com',
|
|
139
|
+
'props': {'orderId': 'AC-4192'},
|
|
140
|
+
})
|
|
141
|
+
except OpenEmailApiError as error:
|
|
142
|
+
if error.is_validation:
|
|
143
|
+
print(error.code, error.param, error.request_id)
|
|
144
|
+
|
|
145
|
+
raise
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
An API refusal is one class, `OpenEmailApiError`, with `status`, `type`, `code`, `param` and `request_id`, plus `is_validation`, `is_not_found`, `is_rate_limited` and friends to branch on. No response at all is `OpenEmailNetworkError`, with `is_timeout` when the deadline passed. Both inherit `OpenEmailError`. An argument the client can tell is wrong before anything is sent, such as a malformed key, raises `ValueError`.
|
|
149
|
+
|
|
150
|
+
### Webhooks
|
|
151
|
+
```python
|
|
152
|
+
import os
|
|
153
|
+
|
|
154
|
+
from fastapi import FastAPI, Request, Response
|
|
155
|
+
from openemail import verify_webhook_signature
|
|
156
|
+
|
|
157
|
+
app = FastAPI()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.post('/webhooks/openemail')
|
|
161
|
+
async def webhook(request: Request) -> Response:
|
|
162
|
+
event = verify_webhook_signature(
|
|
163
|
+
payload=await request.body(),
|
|
164
|
+
headers=request.headers,
|
|
165
|
+
secret=os.environ['OPENEMAIL_WEBHOOK_SECRET'],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
print(event['type'], event['data'])
|
|
169
|
+
|
|
170
|
+
return Response(status_code=204)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It checks the HMAC in constant time and rejects a delivery more than five minutes old, then returns the parsed event, or raises `WebhookVerificationError`. Pass the raw body as bytes or text: re-serialising it changes the bytes and the signature will not match. The headers can come from any framework, since the lookup ignores case.
|
|
174
|
+
|
|
175
|
+
### Disposable inboxes
|
|
176
|
+
```python
|
|
177
|
+
from openemail import create_temp_mail
|
|
178
|
+
|
|
179
|
+
temp = create_temp_mail()
|
|
180
|
+
|
|
181
|
+
inbox = temp.create({'ttlMinutes': 60})
|
|
182
|
+
|
|
183
|
+
messages = temp.list_messages(inbox['id'], inbox_token=inbox['token'])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`create` needs no credential and is the only call that returns the inbox token, so keep it.
|
|
187
|
+
|
|
188
|
+
### OAuth access tokens
|
|
189
|
+
An app a person connected to OpenEmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as `access_token`:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from openemail import OpenEmail
|
|
193
|
+
|
|
194
|
+
client = OpenEmail(access_token=session.fresh_access_token)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`access_token` takes the token itself, or a function that returns it. The function runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. On `AsyncOpenEmail` the function may also be `async`. Pass `api_key` or `access_token`, not both. `OpenEmail()` reads `OPENEMAIL_ACCESS_TOKEN` when you pass neither and `OPENEMAIL_API_KEY` is not set. `me.get()` answers `'object': 'oauth_token'` for a token, with the connected app's `clientId` and `expiresAt`, when the person's approval of the app runs out.
|
|
198
|
+
|
|
199
|
+
A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request fails with `is_step_up_required`. Ask for a code, check it, then replay the request:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from openemail import OpenEmailApiError
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
client.domains.delete(domain_id)
|
|
206
|
+
except OpenEmailApiError as error:
|
|
207
|
+
if not error.is_step_up_required:
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
challenge = client.security.begin_step_up()
|
|
211
|
+
|
|
212
|
+
if challenge['method'] == 'email':
|
|
213
|
+
prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: '
|
|
214
|
+
else:
|
|
215
|
+
prompt = 'Enter the code from your authenticator app, or a backup code: '
|
|
216
|
+
|
|
217
|
+
client.security.verify_step_up({'code': input(prompt)})
|
|
218
|
+
client.domains.delete(domain_id)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
An emailed code works for 10 minutes, and `begin_step_up({'resend': True})` sends a fresh one. Once a code is verified the app is not asked again for 60 minutes. `security.step_up_status()` says whether it is verified right now. API keys are never asked for a code.
|
|
222
|
+
|
|
223
|
+
### Configuring
|
|
224
|
+
Pass keyword arguments when the defaults are not right:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
import os
|
|
228
|
+
|
|
229
|
+
import httpx
|
|
230
|
+
from openemail import OpenEmail
|
|
231
|
+
|
|
232
|
+
client = OpenEmail(
|
|
233
|
+
os.environ['OPENEMAIL_API_KEY'],
|
|
234
|
+
base_url='https://api.openemail.uk',
|
|
235
|
+
timeout=30,
|
|
236
|
+
max_retries=2,
|
|
237
|
+
http_client=httpx.Client(proxy='http://proxy.internal:3128', follow_redirects=True),
|
|
238
|
+
headers={'X-Team': 'billing'},
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The shipped `openemail` client takes the same arguments through `init(...)`, called once at startup.
|
|
243
|
+
|
|
244
|
+
`base_url` also comes from `OPENEMAIL_BASE_URL`. Use an `https:` origin: the client refuses to send an API key, an access token or an inbox token over plain `http:`, and raises before the request leaves, unless the server is on this machine at `localhost`, a `127.x.x.x` address or `::1`. A `base_url` on `0.0.0.0` raises when the client is built, since that is the address a server listens on: use `127.0.0.1` with the same port.
|
|
245
|
+
|
|
246
|
+
`timeout` is in seconds and bounds the whole attempt, the response body included, and `0` turns it off. Reads are retried on 408 and 5xx with backoff. A 429 is retried only when it carries a `Retry-After`, and any wait longer than a minute raises instead of sleeping. Writes that cannot safely repeat are not retried. Every method outside `temp_mail` takes `api_key=` and `timeout=`, so one process can serve several workspaces with one client. The `temp_mail` methods take `inbox_token=` instead of `api_key=`.
|
|
247
|
+
|
|
248
|
+
An endpoint no method wraps yet is one `client.raw.request()` away, with the client's credential, base URL, timeout and retry policy applied:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The path must begin with a single `/`. Anything else, such as `//host/x`, raises before a request is sent, and so does a path whose finished URL leaves the base URL's origin, so the credential it carries never reaches another host.
|
|
255
|
+
|
|
256
|
+
When a newer version is on PyPI the client says so once on a terminal. `OPENEMAIL_DISABLE_UPDATE_NOTICE=1` or `disable_update_notice=True` turns that off.
|