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.
Files changed (123) hide show
  1. openemail-0.0.1/.gitignore +7 -0
  2. openemail-0.0.1/CHANGELOG.md +56 -0
  3. openemail-0.0.1/PKG-INFO +288 -0
  4. openemail-0.0.1/README.md +256 -0
  5. openemail-0.0.1/pyproject.toml +144 -0
  6. openemail-0.0.1/src/openemail/__init__.py +262 -0
  7. openemail-0.0.1/src/openemail/_async/__init__.py +0 -0
  8. openemail-0.0.1/src/openemail/_async/client.py +383 -0
  9. openemail-0.0.1/src/openemail/_async/pagination.py +104 -0
  10. openemail-0.0.1/src/openemail/_async/resources/__init__.py +0 -0
  11. openemail-0.0.1/src/openemail/_async/resources/account.py +226 -0
  12. openemail-0.0.1/src/openemail/_async/resources/addresses.py +89 -0
  13. openemail-0.0.1/src/openemail/_async/resources/analytics.py +88 -0
  14. openemail-0.0.1/src/openemail/_async/resources/app_host.py +72 -0
  15. openemail-0.0.1/src/openemail/_async/resources/audiences.py +374 -0
  16. openemail-0.0.1/src/openemail/_async/resources/billing.py +237 -0
  17. openemail-0.0.1/src/openemail/_async/resources/branding.py +85 -0
  18. openemail-0.0.1/src/openemail/_async/resources/broadcasts.py +315 -0
  19. openemail-0.0.1/src/openemail/_async/resources/calendar.py +215 -0
  20. openemail-0.0.1/src/openemail/_async/resources/chats.py +136 -0
  21. openemail-0.0.1/src/openemail/_async/resources/contacts.py +482 -0
  22. openemail-0.0.1/src/openemail/_async/resources/dns_connections.py +60 -0
  23. openemail-0.0.1/src/openemail/_async/resources/domains.py +613 -0
  24. openemail-0.0.1/src/openemail/_async/resources/drafts.py +153 -0
  25. openemail-0.0.1/src/openemail/_async/resources/emails.py +392 -0
  26. openemail-0.0.1/src/openemail/_async/resources/encryption.py +68 -0
  27. openemail-0.0.1/src/openemail/_async/resources/exports.py +84 -0
  28. openemail-0.0.1/src/openemail/_async/resources/files.py +345 -0
  29. openemail-0.0.1/src/openemail/_async/resources/forms.py +473 -0
  30. openemail-0.0.1/src/openemail/_async/resources/imports.py +284 -0
  31. openemail-0.0.1/src/openemail/_async/resources/keys.py +518 -0
  32. openemail-0.0.1/src/openemail/_async/resources/labels.py +154 -0
  33. openemail-0.0.1/src/openemail/_async/resources/languages.py +32 -0
  34. openemail-0.0.1/src/openemail/_async/resources/me.py +53 -0
  35. openemail-0.0.1/src/openemail/_async/resources/members.py +290 -0
  36. openemail-0.0.1/src/openemail/_async/resources/provider_imports.py +137 -0
  37. openemail-0.0.1/src/openemail/_async/resources/roles.py +156 -0
  38. openemail-0.0.1/src/openemail/_async/resources/rules.py +259 -0
  39. openemail-0.0.1/src/openemail/_async/resources/security.py +64 -0
  40. openemail-0.0.1/src/openemail/_async/resources/senders.py +45 -0
  41. openemail-0.0.1/src/openemail/_async/resources/settings.py +48 -0
  42. openemail-0.0.1/src/openemail/_async/resources/subscriptions.py +134 -0
  43. openemail-0.0.1/src/openemail/_async/resources/support.py +31 -0
  44. openemail-0.0.1/src/openemail/_async/resources/suppressions.py +141 -0
  45. openemail-0.0.1/src/openemail/_async/resources/temp_mail.py +230 -0
  46. openemail-0.0.1/src/openemail/_async/resources/templates.py +618 -0
  47. openemail-0.0.1/src/openemail/_async/resources/threads.py +400 -0
  48. openemail-0.0.1/src/openemail/_async/resources/tools.py +62 -0
  49. openemail-0.0.1/src/openemail/_async/resources/tracking.py +282 -0
  50. openemail-0.0.1/src/openemail/_async/resources/webhooks.py +559 -0
  51. openemail-0.0.1/src/openemail/_async/resources/workspaces.py +94 -0
  52. openemail-0.0.1/src/openemail/_async/transport.py +252 -0
  53. openemail-0.0.1/src/openemail/_build.py +1 -0
  54. openemail-0.0.1/src/openemail/_core/__init__.py +0 -0
  55. openemail-0.0.1/src/openemail/_core/credentials.py +65 -0
  56. openemail-0.0.1/src/openemail/_core/environment.py +34 -0
  57. openemail-0.0.1/src/openemail/_core/errors.py +138 -0
  58. openemail-0.0.1/src/openemail/_core/hosts.py +234 -0
  59. openemail-0.0.1/src/openemail/_core/namespace.py +32 -0
  60. openemail-0.0.1/src/openemail/_core/request_path.py +39 -0
  61. openemail-0.0.1/src/openemail/_core/response.py +201 -0
  62. openemail-0.0.1/src/openemail/_core/retry.py +50 -0
  63. openemail-0.0.1/src/openemail/_core/sentinel.py +16 -0
  64. openemail-0.0.1/src/openemail/_core/update_notice.py +89 -0
  65. openemail-0.0.1/src/openemail/_core/wire.py +133 -0
  66. openemail-0.0.1/src/openemail/_default.py +76 -0
  67. openemail-0.0.1/src/openemail/_helpers/__init__.py +0 -0
  68. openemail-0.0.1/src/openemail/_helpers/languages.py +59 -0
  69. openemail-0.0.1/src/openemail/_helpers/messages.py +13 -0
  70. openemail-0.0.1/src/openemail/_helpers/webhooks.py +115 -0
  71. openemail-0.0.1/src/openemail/_sync/__init__.py +0 -0
  72. openemail-0.0.1/src/openemail/_sync/client.py +383 -0
  73. openemail-0.0.1/src/openemail/_sync/pagination.py +104 -0
  74. openemail-0.0.1/src/openemail/_sync/resources/__init__.py +0 -0
  75. openemail-0.0.1/src/openemail/_sync/resources/account.py +226 -0
  76. openemail-0.0.1/src/openemail/_sync/resources/addresses.py +87 -0
  77. openemail-0.0.1/src/openemail/_sync/resources/analytics.py +88 -0
  78. openemail-0.0.1/src/openemail/_sync/resources/app_host.py +72 -0
  79. openemail-0.0.1/src/openemail/_sync/resources/audiences.py +374 -0
  80. openemail-0.0.1/src/openemail/_sync/resources/billing.py +237 -0
  81. openemail-0.0.1/src/openemail/_sync/resources/branding.py +85 -0
  82. openemail-0.0.1/src/openemail/_sync/resources/broadcasts.py +315 -0
  83. openemail-0.0.1/src/openemail/_sync/resources/calendar.py +215 -0
  84. openemail-0.0.1/src/openemail/_sync/resources/chats.py +136 -0
  85. openemail-0.0.1/src/openemail/_sync/resources/contacts.py +482 -0
  86. openemail-0.0.1/src/openemail/_sync/resources/dns_connections.py +60 -0
  87. openemail-0.0.1/src/openemail/_sync/resources/domains.py +613 -0
  88. openemail-0.0.1/src/openemail/_sync/resources/drafts.py +153 -0
  89. openemail-0.0.1/src/openemail/_sync/resources/emails.py +392 -0
  90. openemail-0.0.1/src/openemail/_sync/resources/encryption.py +68 -0
  91. openemail-0.0.1/src/openemail/_sync/resources/exports.py +84 -0
  92. openemail-0.0.1/src/openemail/_sync/resources/files.py +345 -0
  93. openemail-0.0.1/src/openemail/_sync/resources/forms.py +473 -0
  94. openemail-0.0.1/src/openemail/_sync/resources/imports.py +284 -0
  95. openemail-0.0.1/src/openemail/_sync/resources/keys.py +518 -0
  96. openemail-0.0.1/src/openemail/_sync/resources/labels.py +154 -0
  97. openemail-0.0.1/src/openemail/_sync/resources/languages.py +32 -0
  98. openemail-0.0.1/src/openemail/_sync/resources/me.py +53 -0
  99. openemail-0.0.1/src/openemail/_sync/resources/members.py +290 -0
  100. openemail-0.0.1/src/openemail/_sync/resources/provider_imports.py +137 -0
  101. openemail-0.0.1/src/openemail/_sync/resources/roles.py +156 -0
  102. openemail-0.0.1/src/openemail/_sync/resources/rules.py +259 -0
  103. openemail-0.0.1/src/openemail/_sync/resources/security.py +64 -0
  104. openemail-0.0.1/src/openemail/_sync/resources/senders.py +45 -0
  105. openemail-0.0.1/src/openemail/_sync/resources/settings.py +48 -0
  106. openemail-0.0.1/src/openemail/_sync/resources/subscriptions.py +134 -0
  107. openemail-0.0.1/src/openemail/_sync/resources/support.py +31 -0
  108. openemail-0.0.1/src/openemail/_sync/resources/suppressions.py +141 -0
  109. openemail-0.0.1/src/openemail/_sync/resources/temp_mail.py +230 -0
  110. openemail-0.0.1/src/openemail/_sync/resources/templates.py +618 -0
  111. openemail-0.0.1/src/openemail/_sync/resources/threads.py +400 -0
  112. openemail-0.0.1/src/openemail/_sync/resources/tools.py +62 -0
  113. openemail-0.0.1/src/openemail/_sync/resources/tracking.py +282 -0
  114. openemail-0.0.1/src/openemail/_sync/resources/webhooks.py +559 -0
  115. openemail-0.0.1/src/openemail/_sync/resources/workspaces.py +94 -0
  116. openemail-0.0.1/src/openemail/_sync/transport.py +258 -0
  117. openemail-0.0.1/src/openemail/_version.py +1 -0
  118. openemail-0.0.1/src/openemail/constants/__init__.py +2565 -0
  119. openemail-0.0.1/src/openemail/constants/client.py +78 -0
  120. openemail-0.0.1/src/openemail/py.typed +0 -0
  121. openemail-0.0.1/src/openemail/types/__init__.py +1710 -0
  122. openemail-0.0.1/src/openemail/types/__init__.pyi +5865 -0
  123. openemail-0.0.1/src/openemail/types/client.py +25 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ .ruff_cache/
@@ -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`.
@@ -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.