nextmail 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.
- nextmail-0.0.1/.gitignore +7 -0
- nextmail-0.0.1/CHANGELOG.md +283 -0
- nextmail-0.0.1/PKG-INFO +297 -0
- nextmail-0.0.1/README.md +265 -0
- nextmail-0.0.1/pyproject.toml +144 -0
- nextmail-0.0.1/src/nextmail/__init__.py +566 -0
- nextmail-0.0.1/src/nextmail/_async/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_async/client.py +467 -0
- nextmail-0.0.1/src/nextmail/_async/pagination.py +104 -0
- nextmail-0.0.1/src/nextmail/_async/resources/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_async/resources/account.py +577 -0
- nextmail-0.0.1/src/nextmail/_async/resources/addresses.py +89 -0
- nextmail-0.0.1/src/nextmail/_async/resources/analytics.py +102 -0
- nextmail-0.0.1/src/nextmail/_async/resources/app_host.py +72 -0
- nextmail-0.0.1/src/nextmail/_async/resources/audiences.py +386 -0
- nextmail-0.0.1/src/nextmail/_async/resources/automations.py +567 -0
- nextmail-0.0.1/src/nextmail/_async/resources/billing.py +283 -0
- nextmail-0.0.1/src/nextmail/_async/resources/bookings.py +432 -0
- nextmail-0.0.1/src/nextmail/_async/resources/branding.py +90 -0
- nextmail-0.0.1/src/nextmail/_async/resources/broadcasts.py +424 -0
- nextmail-0.0.1/src/nextmail/_async/resources/calendar.py +541 -0
- nextmail-0.0.1/src/nextmail/_async/resources/chats.py +277 -0
- nextmail-0.0.1/src/nextmail/_async/resources/connected_mailboxes.py +129 -0
- nextmail-0.0.1/src/nextmail/_async/resources/connectors.py +370 -0
- nextmail-0.0.1/src/nextmail/_async/resources/contacts.py +868 -0
- nextmail-0.0.1/src/nextmail/_async/resources/dns_connections.py +74 -0
- nextmail-0.0.1/src/nextmail/_async/resources/domains.py +1084 -0
- nextmail-0.0.1/src/nextmail/_async/resources/drafts.py +155 -0
- nextmail-0.0.1/src/nextmail/_async/resources/emails.py +516 -0
- nextmail-0.0.1/src/nextmail/_async/resources/encryption.py +68 -0
- nextmail-0.0.1/src/nextmail/_async/resources/events.py +87 -0
- nextmail-0.0.1/src/nextmail/_async/resources/exports.py +84 -0
- nextmail-0.0.1/src/nextmail/_async/resources/files.py +332 -0
- nextmail-0.0.1/src/nextmail/_async/resources/follow_ups.py +85 -0
- nextmail-0.0.1/src/nextmail/_async/resources/forms.py +481 -0
- nextmail-0.0.1/src/nextmail/_async/resources/imports.py +370 -0
- nextmail-0.0.1/src/nextmail/_async/resources/keys.py +518 -0
- nextmail-0.0.1/src/nextmail/_async/resources/knowledge.py +550 -0
- nextmail-0.0.1/src/nextmail/_async/resources/labels.py +172 -0
- nextmail-0.0.1/src/nextmail/_async/resources/languages.py +32 -0
- nextmail-0.0.1/src/nextmail/_async/resources/me.py +224 -0
- nextmail-0.0.1/src/nextmail/_async/resources/members.py +386 -0
- nextmail-0.0.1/src/nextmail/_async/resources/out_of_office.py +69 -0
- nextmail-0.0.1/src/nextmail/_async/resources/provider_imports.py +168 -0
- nextmail-0.0.1/src/nextmail/_async/resources/roles.py +156 -0
- nextmail-0.0.1/src/nextmail/_async/resources/rules.py +299 -0
- nextmail-0.0.1/src/nextmail/_async/resources/security.py +154 -0
- nextmail-0.0.1/src/nextmail/_async/resources/segments.py +115 -0
- nextmail-0.0.1/src/nextmail/_async/resources/senders.py +59 -0
- nextmail-0.0.1/src/nextmail/_async/resources/sending.py +74 -0
- nextmail-0.0.1/src/nextmail/_async/resources/settings.py +48 -0
- nextmail-0.0.1/src/nextmail/_async/resources/signatures.py +178 -0
- nextmail-0.0.1/src/nextmail/_async/resources/subscriptions.py +195 -0
- nextmail-0.0.1/src/nextmail/_async/resources/support.py +31 -0
- nextmail-0.0.1/src/nextmail/_async/resources/suppressions.py +203 -0
- nextmail-0.0.1/src/nextmail/_async/resources/tasks.py +439 -0
- nextmail-0.0.1/src/nextmail/_async/resources/temp_mail.py +230 -0
- nextmail-0.0.1/src/nextmail/_async/resources/templates.py +635 -0
- nextmail-0.0.1/src/nextmail/_async/resources/threads.py +858 -0
- nextmail-0.0.1/src/nextmail/_async/resources/tools.py +138 -0
- nextmail-0.0.1/src/nextmail/_async/resources/tracking.py +300 -0
- nextmail-0.0.1/src/nextmail/_async/resources/webhooks.py +681 -0
- nextmail-0.0.1/src/nextmail/_async/resources/workspaces.py +139 -0
- nextmail-0.0.1/src/nextmail/_async/transport.py +252 -0
- nextmail-0.0.1/src/nextmail/_build.py +1 -0
- nextmail-0.0.1/src/nextmail/_core/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_core/credentials.py +65 -0
- nextmail-0.0.1/src/nextmail/_core/crop_query.py +59 -0
- nextmail-0.0.1/src/nextmail/_core/environment.py +34 -0
- nextmail-0.0.1/src/nextmail/_core/errors.py +138 -0
- nextmail-0.0.1/src/nextmail/_core/hosts.py +234 -0
- nextmail-0.0.1/src/nextmail/_core/log_query.py +37 -0
- nextmail-0.0.1/src/nextmail/_core/namespace.py +32 -0
- nextmail-0.0.1/src/nextmail/_core/request_path.py +39 -0
- nextmail-0.0.1/src/nextmail/_core/response.py +201 -0
- nextmail-0.0.1/src/nextmail/_core/retry.py +50 -0
- nextmail-0.0.1/src/nextmail/_core/sentinel.py +16 -0
- nextmail-0.0.1/src/nextmail/_core/timeouts.py +17 -0
- nextmail-0.0.1/src/nextmail/_core/update_notice.py +89 -0
- nextmail-0.0.1/src/nextmail/_core/uploads.py +17 -0
- nextmail-0.0.1/src/nextmail/_core/wire.py +152 -0
- nextmail-0.0.1/src/nextmail/_default.py +76 -0
- nextmail-0.0.1/src/nextmail/_helpers/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_helpers/languages.py +59 -0
- nextmail-0.0.1/src/nextmail/_helpers/messages.py +13 -0
- nextmail-0.0.1/src/nextmail/_helpers/webhooks.py +115 -0
- nextmail-0.0.1/src/nextmail/_sync/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_sync/client.py +467 -0
- nextmail-0.0.1/src/nextmail/_sync/pagination.py +104 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/__init__.py +0 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/account.py +577 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/addresses.py +87 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/analytics.py +102 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/app_host.py +72 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/audiences.py +386 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/automations.py +567 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/billing.py +283 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/bookings.py +432 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/branding.py +90 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/broadcasts.py +424 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/calendar.py +541 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/chats.py +277 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/connected_mailboxes.py +129 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/connectors.py +370 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/contacts.py +868 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/dns_connections.py +74 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/domains.py +1084 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/drafts.py +155 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/emails.py +516 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/encryption.py +68 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/events.py +87 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/exports.py +84 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/files.py +332 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/follow_ups.py +85 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/forms.py +481 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/imports.py +370 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/keys.py +518 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/knowledge.py +550 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/labels.py +172 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/languages.py +32 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/me.py +224 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/members.py +386 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/out_of_office.py +69 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/provider_imports.py +168 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/roles.py +156 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/rules.py +299 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/security.py +154 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/segments.py +115 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/senders.py +59 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/sending.py +74 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/settings.py +48 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/signatures.py +178 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/subscriptions.py +195 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/support.py +31 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/suppressions.py +203 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/tasks.py +439 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/temp_mail.py +230 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/templates.py +635 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/threads.py +858 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/tools.py +138 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/tracking.py +300 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/webhooks.py +681 -0
- nextmail-0.0.1/src/nextmail/_sync/resources/workspaces.py +139 -0
- nextmail-0.0.1/src/nextmail/_sync/transport.py +258 -0
- nextmail-0.0.1/src/nextmail/_version.py +1 -0
- nextmail-0.0.1/src/nextmail/constants/__init__.py +4591 -0
- nextmail-0.0.1/src/nextmail/constants/client.py +79 -0
- nextmail-0.0.1/src/nextmail/py.typed +0 -0
- nextmail-0.0.1/src/nextmail/types/__init__.py +3359 -0
- nextmail-0.0.1/src/nextmail/types/__init__.pyi +11533 -0
- nextmail-0.0.1/src/nextmail/types/client.py +25 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
The first release of the Python package. It has a method for every one of the 746 methods of the
|
|
6
|
+
TypeScript SDK, in 53 namespaces, each under its TypeScript name in snake_case, and a parity check
|
|
7
|
+
in the package's own build proves that every one sends the same request as its TypeScript twin.
|
|
8
|
+
|
|
9
|
+
- Two clients with the same methods and arguments: `Nextmail`, which blocks, and `AsyncNextmail`,
|
|
10
|
+
which is awaited and runs on asyncio and on trio. Each has an attribute for every namespace, from
|
|
11
|
+
`emails`, `threads` and `templates` to `automations`, `bookings` and `connected_mailboxes`.
|
|
12
|
+
- A request body is a dict whose keys keep the API's camelCase names, the options of a call are
|
|
13
|
+
snake_case keyword arguments such as `idempotency_key=` and `limit=`, and a response comes back as
|
|
14
|
+
a dict. Every body, result and webhook payload has a `TypedDict` in `nextmail.types`, so a type
|
|
15
|
+
checker catches a misspelt key.
|
|
16
|
+
- A client takes an API key or an OAuth access token, or reads `NEXTMAIL_API_KEY`,
|
|
17
|
+
`NEXTMAIL_ACCESS_TOKEN` and `NEXTMAIL_BASE_URL`. `access_token=` also takes a function that
|
|
18
|
+
returns the token, called before every request, which may be async on `AsyncNextmail`.
|
|
19
|
+
`base_url`, `timeout`, `max_retries`, `headers`, `user_agent` and `http_client`, your own `httpx`
|
|
20
|
+
client, set the rest. Every method also takes `timeout=` for one call, and `api_key=`, or
|
|
21
|
+
`inbox_token=` on `temp_mail`, to act with another credential.
|
|
22
|
+
- Every workspace is the same kind, the one that holds the free address included. That address
|
|
23
|
+
sends only the mail a person writes in the app, so a send from it with an API key or an access
|
|
24
|
+
token is refused with `from_address_forbidden`.
|
|
25
|
+
- `from nextmail import nextmail` is a ready-made client, built from the environment the first time
|
|
26
|
+
it is touched, and `init(...)` configures it once at startup. `create_temp_mail()` and
|
|
27
|
+
`create_async_temp_mail()` open disposable inboxes with no credential.
|
|
28
|
+
- Every paginated resource has `list` for one page, `list_all` for every item as a list, and
|
|
29
|
+
`iterate`, which yields each item and fetches the next page only when it gets there.
|
|
30
|
+
- An API refusal raises `NextmailApiError`, with `status`, `type`, `code`, `param`, `doc_url`,
|
|
31
|
+
`request_id`, `retry_after_seconds` and `fields`, and checks such as `is_validation`,
|
|
32
|
+
`is_not_found`, `is_rate_limited`, `is_step_up_required` and `is_retryable`. No response at all
|
|
33
|
+
raises `NextmailNetworkError`, with `is_timeout` when the deadline passed. Both derive from
|
|
34
|
+
`NextmailError`, and a mistake in the call itself raises `ValueError` before anything is sent.
|
|
35
|
+
- Reads, sends and every write that is safe to repeat are retried on 408, 500, 502, 503 and 504 with
|
|
36
|
+
backoff, and on a 429 only when its `Retry-After` is a minute or less. A send carries an
|
|
37
|
+
idempotency key that its retries reuse, so a retry never sends a second message. `timeout` bounds
|
|
38
|
+
a whole attempt, the response body included, and uploads wait at least ten minutes.
|
|
39
|
+
- `verify_webhook_signature` checks a delivery in constant time, refuses one more than five minutes
|
|
40
|
+
old and returns the parsed event, or raises `WebhookVerificationError`.
|
|
41
|
+
- The helpers of the TypeScript SDK are here too, such as `to_base64` and `resolve_language`, and
|
|
42
|
+
every value set it exports is a constant in `nextmail.constants`, such as `WEBHOOK_EVENTS` and
|
|
43
|
+
`API_SCOPES`.
|
|
44
|
+
- `client.raw.request()` reaches any endpoint with the client's credential, base URL, timeout and
|
|
45
|
+
retries, and a credential is never sent over plain HTTP, except to a server on this machine.
|
|
46
|
+
- When a newer version is on PyPI, the client says so once, on a terminal only.
|
|
47
|
+
`disable_update_notice=True` or `NEXTMAIL_DISABLE_UPDATE_NOTICE=1` turns that off.
|
|
48
|
+
- It needs Python 3.10 or newer and depends only on `httpx`, `anyio` and `typing-extensions`.
|
|
49
|
+
- API keys begin `nm_live_` or `nm_test_`, and temporary inbox tokens `nm_inbox_`. The client
|
|
50
|
+
refuses a key of any other shape.
|
|
51
|
+
- Images up to 100 MB go in on `account.set_photo`, `contacts.set_photo`,
|
|
52
|
+
`domains.set_address_photo`, `bookings.upload_background`, `branding.upload_image` and
|
|
53
|
+
`templates.upload_image`. Each also takes `crop=`, the part of the image to keep as an
|
|
54
|
+
`ImageCrop` of fractions, and waits as long as `files.upload` does.
|
|
55
|
+
- `emails.list_events_by_message_id`, `list_all_events_by_message_id` and
|
|
56
|
+
`iterate_events_by_message_id` read the event trail of a sent email found by the Message-ID header
|
|
57
|
+
it went out with, for a caller that holds that header and not the `msg_` id, such as a reply or a
|
|
58
|
+
bounce report that quotes it. They take the Message-ID as their first argument, sent as
|
|
59
|
+
`messageId`, with `limit=`, `cursor=` and `address=` as on `list_events`. A Message-ID that no
|
|
60
|
+
message the key reaches went out with is a 404.
|
|
61
|
+
- `provider_imports.list`, `list_all` and `iterate` take `source=` (one of
|
|
62
|
+
`PROVIDER_IMPORT_PROVIDERS`, typed as `ProviderImportProvider`), `status=` (one of
|
|
63
|
+
`IMPORT_HISTORY_STATUSES`, typed as `ImportHistoryStatus`) and `search=`, up to 320 characters,
|
|
64
|
+
the filters `imports.list` takes, sent with every page. An import from a provider never pauses, so
|
|
65
|
+
`status='paused'` answers an empty page.
|
|
66
|
+
- `emails.list_events`, `list_all_events` and `iterate_events` take `message_id=`, 1 to 998
|
|
67
|
+
characters, sent as `messageId`: the trail answers only when the send went out with that
|
|
68
|
+
Message-ID header, with or without its angle brackets, and is a 404 otherwise. The send id is
|
|
69
|
+
still required.
|
|
70
|
+
- `files.create_link` takes `reuse=`: `True` hands back the working public link the file already
|
|
71
|
+
has, with its own domain and download count, instead of making another, and a file with none gets
|
|
72
|
+
a new one. A link handed back answers 200 and a new one 201.
|
|
73
|
+
- `keys.create`, `update` and `delete`, `keys.rotate` for any key but the caller, and
|
|
74
|
+
`workspaces.offer_ownership`, `withdraw_ownership_offer`, `account.accept_ownership_offer` and
|
|
75
|
+
`decline_ownership_offer` refuse an API key with 403 `console_only`, `ERROR_CODES.CONSOLE_ONLY` in
|
|
76
|
+
`nextmail.constants`. The console confirms these changes with a verification code, which a key
|
|
77
|
+
cannot give, so they are made there. A key still rotates its own secret with `keys.rotate` or
|
|
78
|
+
`me.rotate`, and revokes keys with `keys.revoke`.
|
|
79
|
+
- `app_host.get`, `set` and `verify` return `sender`, typed as `AppHostSender` in `nextmail.types`:
|
|
80
|
+
where the emails sent to the people of the workspace come from, a dict with `address`, `domain`
|
|
81
|
+
and `waitingOn`. `waitingOn` is `logo` or `signing`, one of `APP_HOST_SENDER_WAITS` and typed as
|
|
82
|
+
`AppHostSenderWait`, until they come from `noreply@` the domain of the live web app address, then
|
|
83
|
+
`None`. `sender` is `None` on the free plan and until an address on a verified domain of the
|
|
84
|
+
workspace is live.
|
|
85
|
+
- `addresses.list`, `list_all` and `iterate` give each address a `role`, one of `ADDRESS_ROLES` and
|
|
86
|
+
typed as `AddressRole`: `owner` on every address for an API key, and `member` or `viewer` for an
|
|
87
|
+
app a member connected. A key narrowed to particular addresses or domains lists only the addresses
|
|
88
|
+
and domains it covers, and the free address of the person the key acts for is listed, with `role`
|
|
89
|
+
set to `owner`, by a key of the workspace it belongs to.
|
|
90
|
+
- `chats.send_message` says something in a chat with the assistant, or starts one, and returns the
|
|
91
|
+
turn once the assistant has replied, typed as `ChatTurnResource` in `nextmail.types`: its
|
|
92
|
+
`status`, one of `CHAT_TURN_STATUSES`, the `messages` it added, `stopReason` and the
|
|
93
|
+
`pendingToolCalls` that wait for an answer. `chats.approve_tool_call` runs one, with `args` laid
|
|
94
|
+
over the ones the assistant chose, `chats.deny_tool_call` refuses one, and `chats.stop_turn`
|
|
95
|
+
stops a turn and answers `stopped` or `idle`, from `CHAT_STOP_STATUSES`. The three calls that
|
|
96
|
+
wait for the assistant wait up to 300 seconds, or the client's `timeout` when that is longer,
|
|
97
|
+
unless `timeout=` says otherwise, and `timeout=0` waits as long as it takes. None of the four is
|
|
98
|
+
retried. The refusals are 409 `chat_busy` and `approval_pending`, 403 `tool_not_offered` and 503
|
|
99
|
+
`assistant_unavailable`.
|
|
100
|
+
- `chats.list_messages`, `list_all_messages` and `iterate_messages` read the messages of a chat,
|
|
101
|
+
from the first or, with `order='desc'`, from the newest, each with its `text` and its `parts`,
|
|
102
|
+
typed as `ChatMessageResource`. `CHAT_MESSAGE_ORDERS`, `CHAT_MESSAGE_ROLES`,
|
|
103
|
+
`CHAT_MESSAGE_PART_TYPES`, `CHAT_TOOL_CALL_STATUSES` and `CHAT_STOP_REASONS` hold the values.
|
|
104
|
+
- `members.get_directory_sync`, `connect_directory_sync` and `disconnect_directory_sync` read, turn
|
|
105
|
+
on and turn off directory sync (SCIM), through which a company directory adds people and takes
|
|
106
|
+
them out again. `connect_directory_sync` returns the token once. `members.list_directory_people`
|
|
107
|
+
pages through the people it synced with `start_index=` and `count=`, each in one of
|
|
108
|
+
`DIRECTORY_PERSON_STATES`, and `members.set_directory_default_role` and `set_directory_group_role`
|
|
109
|
+
choose the roles it gives. Only the owner or a role holding `workspace:manage` reaches them, and
|
|
110
|
+
every change asks an OAuth access token for a verification code.
|
|
111
|
+
- `security.get_policy` and `security.update_policy` read and set whether everyone in the workspace
|
|
112
|
+
has to sign in with two-factor authentication, typed as `SecurityPolicyResource`. `get_policy`
|
|
113
|
+
needs no scope and also says whether the person behind the client meets the rule, and
|
|
114
|
+
`update_policy` needs `members:write` and a verification code.
|
|
115
|
+
- `workspaces.get_ownership_offer`, `offer_ownership` and `withdraw_ownership_offer` offer the
|
|
116
|
+
workspace to one of its members, and `account.list_ownership_offers`, `accept_ownership_offer` and
|
|
117
|
+
`decline_ownership_offer` answer an offer. Offering and accepting ask for a verification code.
|
|
118
|
+
- `account.set_name` changes the name of the person. `account.list_push_devices`,
|
|
119
|
+
`remove_push_device` and `send_test_push` read and manage the phones that get their
|
|
120
|
+
notifications, with `platform` from `PUSH_PLATFORMS`, and
|
|
121
|
+
`account.end_connected_app_elevation` makes a connected app ask for verification codes again.
|
|
122
|
+
- `threads.set_replying` and `clear_replying` show the team that you are writing a reply.
|
|
123
|
+
`threads.list_tracking` reads how the mail sent in a thread was read, `threads.get_delivery`
|
|
124
|
+
whether it arrived, typed as `ThreadDeliveryResource`, and `threads.list_file_links`,
|
|
125
|
+
`list_all_file_links` and `iterate_file_links` the download links one message carried, with
|
|
126
|
+
`include_revoked=`. `threads.counts` takes `hide_labelled=`.
|
|
127
|
+
- `senders.get_avatar` returns the picture found for a sender as `bytes`, and `senders.get` and
|
|
128
|
+
`research` report `avatarUrl` and `avatarKind`, one of `SENDER_AVATAR_KINDS`.
|
|
129
|
+
- `dns_connections.start` returns the link that connects a Cloudflare account in a browser,
|
|
130
|
+
`domains.start_gravatar_publish` the link that publishes the photo of an address to Gravatar,
|
|
131
|
+
and `domains.create_mail_app_profile_link` a one-time link that sets an address up on an Apple
|
|
132
|
+
device.
|
|
133
|
+
- `emails.send` and `emails.send_batch` take `isForward` and `originalMessage`, the email being
|
|
134
|
+
forwarded or answered, quoted below the body, and `encryption`, typed as `SealedMessageInput`,
|
|
135
|
+
for a message you sealed yourself with OpenPGP. `emails.translate` takes `skipQuoted` and
|
|
136
|
+
reports `quotedHistoryKept`.
|
|
137
|
+
- `emails.get_tracking`, `list_events`, `list_all_events` and `iterate_events` take `address=`, and
|
|
138
|
+
so do `tracking.list_opens`, `list_clicks` and their walks. `tracking.list`, `list_all` and
|
|
139
|
+
`iterate` take `offset_minutes=`, and `tracking.get_stats` reports `downloads`, typed as
|
|
140
|
+
`TrackingDownloads`.
|
|
141
|
+
- `contacts.list`, `list_all` and `iterate` take `audience_id=`, and `contacts.activity` takes
|
|
142
|
+
`from_=` and `to=`. `audiences.list_contacts` and its walks take `inactive_days=`.
|
|
143
|
+
`audiences.growth`, `bookings.analytics` and `forms.analytics` take `since=` and `until=`, and
|
|
144
|
+
`broadcasts.analytics` and `broadcasts.stats` take `since=`. `imports.list` and its walks take
|
|
145
|
+
`source=`, `status=`, one of `IMPORT_HISTORY_STATUSES`, and `search=`, `rules.list_runs` and its
|
|
146
|
+
walks take `rule_ids=`, and `signatures.list` and its walks take `from_=`.
|
|
147
|
+
- `audiences.add_contact` and `contacts.set_audiences` take `createContact`, which saves an address
|
|
148
|
+
that is not a contact yet first and needs `contacts:write`. Through an app a member connected,
|
|
149
|
+
an address another member saved is a 409 `contact_exists` there and on `contacts.save`.
|
|
150
|
+
- `templates.update` and `templates.replace_content` take `expectedUpdatedAt`, and
|
|
151
|
+
`templates.publish` takes `expected_updated_at=`: the `updatedAt` every template version reports.
|
|
152
|
+
A draft saved again in between is a 409 `version_conflict`.
|
|
153
|
+
- `drafts.create` and `drafts.update` take `attachments`: inline files of up to 5 MB, with
|
|
154
|
+
`content` as `bytes` or base64, or stored files by `fileId`. Every draft attachment reports
|
|
155
|
+
`fileId`.
|
|
156
|
+
- An attachment given as `{'url': ...}` is sent as it is, with its `filename` and `contentType` when
|
|
157
|
+
they are set.
|
|
158
|
+
- Scopes: `addresses.list` and its walks need `emails:read` or `emails:send`,
|
|
159
|
+
`audiences.list_contacts` and its walks need `contacts:read` too, `emails.compose`, `rewrite`,
|
|
160
|
+
`suggest_subject` and `translate` take `drafts:write` in place of `emails:send`,
|
|
161
|
+
`domains.get_mail_app_settings` and `list_app_passwords` need `threads:read`, and
|
|
162
|
+
`domains.update_mail_app_settings` needs `threads:read` as well as `domains:write`.
|
|
163
|
+
`domains.create_address` asks an OAuth access token for a verification code when it chooses where
|
|
164
|
+
the mail goes or deletion on expiry.
|
|
165
|
+
- `domains.create_address` and `domains.update_address` take `expiresAt` and `onExpiry`, one of
|
|
166
|
+
`ADDRESS_EXPIRY_ACTIONS`, and every address reports them with `expiredAt`. From its `expiresAt` an
|
|
167
|
+
address takes no mail and sends none, a send from it is refused with 403 `address_expired`, and
|
|
168
|
+
within a minute it is switched off, or deleted with the conversations delivered to it when
|
|
169
|
+
`onExpiry` is `delete`. A later `expiresAt`, `None` or `enabled` set to `True` turns a stopped
|
|
170
|
+
address back on. `WEBHOOK_EVENTS.ADDRESS_EXPIRED` is the event `address.expired`, typed as
|
|
171
|
+
`AddressExpiredEventData` in `nextmail.types`.
|
|
172
|
+
- `DOMAIN_ADDRESS_DESTINATIONS` includes `WEBHOOK` and `GROUP`. The same two methods take
|
|
173
|
+
`webhookId` for a webhook destination, `tapWebhookIds` to copy every message to up to 5 endpoints,
|
|
174
|
+
and `groupPosting`, one of `ADDRESS_GROUP_POSTING_POLICIES`, and `groupKeepCopy` for a group, and
|
|
175
|
+
every address reports them. The refusals are `webhook_not_found` and `too_many_taps`.
|
|
176
|
+
- `WEBHOOK_EVENTS.EMAIL_ROUTED` is the event `email.routed`, typed as `EmailRoutedEventData`: a
|
|
177
|
+
message that arrived at an address whose destination is `webhook`, or that a route of its domain
|
|
178
|
+
sent to a webhook, was kept for 30 days instead of being filed. It goes only to the endpoint the
|
|
179
|
+
address or route names and to the taps of the address, and always carries `content` and `raw`.
|
|
180
|
+
`routedBy`, whose `kind` is one of `WEBHOOK_ROUTED_SOURCES`, says which sent it, `recipient`
|
|
181
|
+
keeps the plus tag and `verdicts` are what the receiving server found. An `email.received`
|
|
182
|
+
event carries `tap`, `True` when an endpoint gets the message as a tap of the address rather
|
|
183
|
+
than through its subscription.
|
|
184
|
+
- `domains.list_routes`, `list_all_routes` and `iterate_routes` read the pattern routes of a
|
|
185
|
+
domain in the order they are tried, typed as `AddressRouteResource` in `nextmail.types`.
|
|
186
|
+
`domains.create_route`, `get_route`, `update_route` and `delete_route` manage them: a route
|
|
187
|
+
catches mail to addresses nobody created whose local part matches its `pattern`, such as
|
|
188
|
+
`support-*`, and delivers it to an address, forwards it once the destination confirms, posts it
|
|
189
|
+
to a webhook as `email.routed` or reads it on another domain, the actions
|
|
190
|
+
`ADDRESS_ROUTE_ACTIONS` lists. `active` and `inactiveReason`, one of
|
|
191
|
+
`ADDRESS_ROUTE_INACTIVE_REASONS`, say whether a route takes mail now. A domain holds at most 100
|
|
192
|
+
routes, and the refusals are `invalid_route_pattern`, `route_pattern_taken`,
|
|
193
|
+
`route_target_not_found` and `route_limit_reached`. `domains.test_route` answers which step
|
|
194
|
+
decides mail to an address without sending anything, one of `ADDRESS_ROUTE_TEST_STEPS`, with the
|
|
195
|
+
route that decided and where the mail would go, typed as `AddressRouteTestResource`.
|
|
196
|
+
- `domains.list_group_members`, `list_all_group_members` and `iterate_group_members` read the
|
|
197
|
+
group members of a group address, typed as `GroupMemberResource`, and
|
|
198
|
+
`domains.add_group_members` and `remove_group_member` change them. A group member is an address
|
|
199
|
+
of the workspace or an address at another provider, which confirms by email first, the kinds
|
|
200
|
+
`GROUP_MEMBER_KINDS` lists. A group holds at most 1,000 group members and groups nest at most 3
|
|
201
|
+
deep, and the refusals are `not_a_group`, `group_member_limit`, `group_member_not_found` and
|
|
202
|
+
`group_nesting_too_deep`.
|
|
203
|
+
- `domains.update` takes `aliasOf`, which makes the domain an alias of another domain of the
|
|
204
|
+
workspace, and every domain reports `aliasOf` and `aliases`, typed as `DomainAlias`. The
|
|
205
|
+
refusals are `alias_not_allowed`, `alias_chain` and `alias_has_addresses`, and creating an
|
|
206
|
+
address on an alias is refused with `alias_has_addresses`.
|
|
207
|
+
- `domains.update_address_limits` takes `receiving['storageBytes']`, a storage quota for one
|
|
208
|
+
address from 1 MiB to 1 TiB, and the limits report it. A message that would take the address
|
|
209
|
+
past its quota gets a permanent bounce (5.2.2), and deleting mail for good makes room again.
|
|
210
|
+
`domains.get_address_limits` reports what the address keeps in `usage['storage']`, with `bytes`
|
|
211
|
+
and `messages`, typed as `AddressStorageUsage` in `nextmail.types`. Mail stored before storage
|
|
212
|
+
counting began and mail routed to a webhook are not counted.
|
|
213
|
+
- `webhooks.list_dead_letters`, `list_all_dead_letters` and `iterate_dead_letters` read the events
|
|
214
|
+
an endpoint gave up on: each event whose last automatic attempt failed, with how many times it
|
|
215
|
+
was tried, the last answer and that attempt, typed as `WebhookDeadLetterResource`. `status=`
|
|
216
|
+
reads the `open` ones by default, or `replayed`, `discarded` or `all` from
|
|
217
|
+
`WEBHOOK_DEAD_LETTER_FILTERS`, and `WEBHOOK_DEAD_LETTER_STATUSES` holds the statuses a dead
|
|
218
|
+
letter can have. A dead letter is kept 30 days.
|
|
219
|
+
- `webhooks.replay_dead_letters` sends up to 100 open dead letters again in one call, the ones
|
|
220
|
+
named in `body['ids']` or the oldest, and returns an `outcome` for each from
|
|
221
|
+
`WEBHOOK_DEAD_LETTER_REPLAY_OUTCOMES`. A delivered one is closed as `replayed`.
|
|
222
|
+
`webhooks.discard_dead_letter` closes one without sending it, and one already replayed is 409
|
|
223
|
+
`dead_letter_resolved`.
|
|
224
|
+
- `webhooks.create` and `webhooks.update` take `retryPolicy` from `WEBHOOK_RETRY_POLICIES`:
|
|
225
|
+
`standard` tries a failed delivery up to 8 times over about 27 and a half hours, and `extended` up
|
|
226
|
+
to 16 times over about 3 days. They also take `includeRaw`, which needs `threads:read`, and every
|
|
227
|
+
endpoint reports both.
|
|
228
|
+
- `threads.get_raw_message` returns the source of one message as `message/rfc822` `bytes`, ready
|
|
229
|
+
to save as an `.eml` file. It is the message byte for byte as it arrived when the address had
|
|
230
|
+
`keepOriginal` on, and otherwise a source rebuilt from the stored message, the same bytes the
|
|
231
|
+
IMAP server gives a mail app. The `X-Message-Source` response header says which, one of
|
|
232
|
+
`RAW_MESSAGE_SOURCES`.
|
|
233
|
+
- `domains.create_address` and `domains.update_address` take `keepOriginal`, and every address
|
|
234
|
+
reports it. While it is on, each message the address receives is also kept byte for byte as it
|
|
235
|
+
arrived, and it is deleted with its message.
|
|
236
|
+
- An `email.received` event to an endpoint with `includeRaw` on carries `data['raw']`, typed as
|
|
237
|
+
`WebhookMessageRaw`: a link that downloads the message source for an hour, its `size` and its
|
|
238
|
+
`source`, `original` or `rebuilt`.
|
|
239
|
+
- `domains.get_address_limits` and `domains.update_address_limits` read and change what one address
|
|
240
|
+
may send and receive: a hold that pauses or disables sending or receiving, caps per hour, per 24
|
|
241
|
+
hours and per month on messages and recipients, recipients per message, and lists of allowed and
|
|
242
|
+
blocked recipients and allowed senders, with the usage so far. `ADDRESS_HOLDS` lists the holds.
|
|
243
|
+
- `domains.get` and `domains.update` give each DNS record `found`, what public DNS answers at that
|
|
244
|
+
name now, and `problem`, a dict with a `code` from `DOMAIN_RECORD_PROBLEMS` and a `message` to
|
|
245
|
+
show a person.
|
|
246
|
+
- `domains.update` takes `autodiscovery` in the patch: `True` offers the SRV and TXT records that
|
|
247
|
+
let mail, calendar and contacts apps find the servers from an address on the domain alone, listed
|
|
248
|
+
in `autodiscoveryRecords`.
|
|
249
|
+
- `webhooks.create` and `webhooks.update` take `includeContent`, which puts the message into every
|
|
250
|
+
`email.received` event as `data['content']`: the text, the HTML up to 512 KiB and each attachment
|
|
251
|
+
with a link that downloads it for an hour. Turning it on needs `threads:read`.
|
|
252
|
+
- A connected mailbox can be the sender of `emails.send` with an API key, of broadcasts,
|
|
253
|
+
automations and sign-up forms, and a key or an app can send from the free address of the person
|
|
254
|
+
it acts for. Only that person may send as a free address.
|
|
255
|
+
- `threads.counts()` leaves labelled mail out of `team.assigned` and `team.unassigned`, as the inbox
|
|
256
|
+
does, unless the person has chosen to show labelled mail in the inbox.
|
|
257
|
+
- `domains`, `members`, `keys`, `webhooks` and `rules` take `q=` on `list`, `list_all` and
|
|
258
|
+
`iterate`, and so does `connected_mailboxes.list`. It searches every field a person reads on the
|
|
259
|
+
row: names, addresses, ids, scopes, events, condition values. Every word has to appear, ignoring
|
|
260
|
+
case and accents, and close spellings count when nothing matches exactly.
|
|
261
|
+
- `rules.stats()` reads how the rules did inside a window: messages matched and failed, a series of
|
|
262
|
+
buckets, the matches of each rule, the actions taken, the failures and the senders matched most.
|
|
263
|
+
`rule_ids=` counts some rules only, and the result is typed as `RuleStatsResource` in
|
|
264
|
+
`nextmail.types`.
|
|
265
|
+
- `billing.get()` reports `usage.addressChecks`: the deep address checks used this calendar month,
|
|
266
|
+
what the plan includes and when they start again, beside `payAsYouGo.coversAddressChecks` and
|
|
267
|
+
`extraAddressChecks`. `billing.list_plans()` gives each plan's `monthlyAddressChecks`, and
|
|
268
|
+
`billing.get_pay_as_you_go()` adds `prices.addressChecks` and `period.addressChecks`, all typed in
|
|
269
|
+
`nextmail.types`.
|
|
270
|
+
- `tools.check_address` and `tools.check_addresses` with `deep=True` first use the deep checks the
|
|
271
|
+
plan includes each month (10 on Free, 100 on Starter, 500 on Business, 1,000 on Enterprise), then
|
|
272
|
+
pay as you go at $1.50 per 100 addresses.
|
|
273
|
+
- An automation branch can check the contact's address: `{'kind': 'address_check', 'mailbox': ...,
|
|
274
|
+
'accept': ...}`, with `accept` from `nextmail.AUTOMATION_ADDRESS_ACCEPTS` and the condition typed as
|
|
275
|
+
`AutomationAddressCondition` in `nextmail.types`.
|
|
276
|
+
- A send leaves out an address whose domain takes no mail, and one the check on sending finds has no
|
|
277
|
+
mailbox. Each shows as `suppressed`, with an `email.suppressed` webhook that names the reason,
|
|
278
|
+
never as a bounce.
|
|
279
|
+
- Forms check the address of every sign-up: `forms.subscribe` refuses one whose domain takes no mail
|
|
280
|
+
with the field error `undeliverable`. `settings['refuseDisposable']` also refuses throwaway
|
|
281
|
+
addresses (`disposable`), `settings['checkMailbox']` runs a deep check on every sign-up and
|
|
282
|
+
refuses a missing mailbox (`mailbox_not_found`), and a refused address that looks like a typo
|
|
283
|
+
carries `suggestion`.
|
nextmail-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nextmail
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The official Python SDK for the Nextmail 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://nextmail.sh
|
|
6
|
+
Project-URL: Documentation, https://nextmail.sh/docs/python
|
|
7
|
+
Project-URL: Reference, https://nextmail.sh/docs/python/reference/methods
|
|
8
|
+
Project-URL: Changelog, https://nextmail.sh/docs/python/changelog
|
|
9
|
+
Project-URL: Support, https://nextmail.sh/contact
|
|
10
|
+
Author: Nextmail
|
|
11
|
+
License-Expression: LicenseRef-Proprietary
|
|
12
|
+
Keywords: api,api client,email,email api,email templates,inbox,mailbox,nextmail,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://nextmail.sh'>
|
|
35
|
+
<img
|
|
36
|
+
src='https://nextmail.sh/logo.svg'
|
|
37
|
+
alt='Nextmail 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://nextmail.sh'>
|
|
51
|
+
<b>
|
|
52
|
+
Website
|
|
53
|
+
</b>
|
|
54
|
+
</a>
|
|
55
|
+
•
|
|
56
|
+
<a href='https://nextmail.sh/docs/python'>
|
|
57
|
+
<b>
|
|
58
|
+
Documentation
|
|
59
|
+
</b>
|
|
60
|
+
</a>
|
|
61
|
+
•
|
|
62
|
+
<a href='https://nextmail.sh/docs/python/reference/methods'>
|
|
63
|
+
<b>
|
|
64
|
+
Every method
|
|
65
|
+
</b>
|
|
66
|
+
</a>
|
|
67
|
+
•
|
|
68
|
+
<a href='https://nextmail.sh/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 Nextmail API. It has one method for every method of the TypeScript SDK, under the same names in snake_case, 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 nextmail
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or `uv add nextmail`, or `poetry add nextmail`.
|
|
89
|
+
|
|
90
|
+
### Using
|
|
91
|
+
```python
|
|
92
|
+
from nextmail import Nextmail
|
|
93
|
+
|
|
94
|
+
client = Nextmail('nm_live_...')
|
|
95
|
+
|
|
96
|
+
sent = client.emails.send(
|
|
97
|
+
{
|
|
98
|
+
'from': 'Acme Billing <billing@acme.com>',
|
|
99
|
+
'to': 'ada@example.com',
|
|
100
|
+
'subject': 'Your September invoice',
|
|
101
|
+
'html': '<p>Your invoice is attached.</p>',
|
|
102
|
+
'attachments': [
|
|
103
|
+
{'filename': 'invoice.pdf', 'content': pdf_bytes, 'contentType': 'application/pdf'}
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
print(sent['id'], sent['status'])
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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 `nextmail.types`, so your editor completes the keys and a type checker catches a misspelt one.
|
|
112
|
+
|
|
113
|
+
Create a key in Nextmail under Settings, API keys. It is shown once, and it belongs in an environment variable rather than in code. `Nextmail()` with no key reads `NEXTMAIL_API_KEY`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from nextmail import Nextmail
|
|
117
|
+
|
|
118
|
+
client = Nextmail()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
Or skip even that. The package ships a ready made `nextmail` client that reads `NEXTMAIL_API_KEY` the first time it is touched:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from nextmail import nextmail
|
|
127
|
+
|
|
128
|
+
nextmail.emails.send({'from': sender, 'to': recipient, 'subject': subject, 'text': text})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
### Async
|
|
134
|
+
```python
|
|
135
|
+
import asyncio
|
|
136
|
+
|
|
137
|
+
from nextmail import AsyncNextmail
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def main() -> None:
|
|
141
|
+
async with AsyncNextmail() as client:
|
|
142
|
+
sent = await client.emails.send(
|
|
143
|
+
{'from': sender, 'to': recipient, 'subject': 'Hi', 'text': 'Hello'}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async for thread in client.threads.iterate(folder='inbox'):
|
|
147
|
+
print(thread['id'])
|
|
148
|
+
|
|
149
|
+
print(sent['status'])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
asyncio.run(main())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`AsyncNextmail` has every method `Nextmail` has, with the same arguments, and runs on asyncio and trio.
|
|
156
|
+
|
|
157
|
+
### Reading the mailbox
|
|
158
|
+
```python
|
|
159
|
+
page = client.threads.list(folder='inbox', limit=25)
|
|
160
|
+
|
|
161
|
+
for thread in client.threads.iterate(folder='inbox', query='invoice'):
|
|
162
|
+
full = client.threads.get(thread['id'])
|
|
163
|
+
|
|
164
|
+
print(full['messageCount'], full['hasUnread'])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
### Errors
|
|
170
|
+
```python
|
|
171
|
+
from nextmail import NextmailApiError, nextmail
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
nextmail.templates.send(
|
|
175
|
+
'order-shipped',
|
|
176
|
+
{
|
|
177
|
+
'from': 'dispatch@acme.com',
|
|
178
|
+
'to': 'ada@example.com',
|
|
179
|
+
'props': {'orderId': 'AC-4192'},
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
except NextmailApiError as error:
|
|
183
|
+
if error.is_validation:
|
|
184
|
+
print(error.code, error.param, error.request_id)
|
|
185
|
+
|
|
186
|
+
raise
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
An API refusal is one class, `NextmailApiError`, 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 `NextmailNetworkError`, with `is_timeout` when the deadline passed. Both inherit `NextmailError`. An argument the client can tell is wrong before anything is sent, such as a malformed key, raises `ValueError`.
|
|
190
|
+
|
|
191
|
+
### Webhooks
|
|
192
|
+
```python
|
|
193
|
+
import os
|
|
194
|
+
|
|
195
|
+
from fastapi import FastAPI, Request, Response
|
|
196
|
+
from nextmail import verify_webhook_signature
|
|
197
|
+
|
|
198
|
+
app = FastAPI()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.post('/webhooks/nextmail')
|
|
202
|
+
async def webhook(request: Request) -> Response:
|
|
203
|
+
event = verify_webhook_signature(
|
|
204
|
+
payload=await request.body(),
|
|
205
|
+
headers=request.headers,
|
|
206
|
+
secret=os.environ['NEXTMAIL_WEBHOOK_SECRET'],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
print(event['type'], event['data'])
|
|
210
|
+
|
|
211
|
+
return Response(status_code=204)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
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.
|
|
215
|
+
|
|
216
|
+
### Disposable inboxes
|
|
217
|
+
```python
|
|
218
|
+
from nextmail import create_temp_mail
|
|
219
|
+
|
|
220
|
+
temp = create_temp_mail()
|
|
221
|
+
|
|
222
|
+
inbox = temp.create({'ttlMinutes': 60})
|
|
223
|
+
|
|
224
|
+
messages = temp.list_messages(inbox['id'], inbox_token=inbox['token'])
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`create` needs no credential and is the only call that returns the inbox token, so keep it.
|
|
228
|
+
|
|
229
|
+
### OAuth access tokens
|
|
230
|
+
An app a person connected to Nextmail 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`:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from nextmail import Nextmail
|
|
234
|
+
|
|
235
|
+
client = Nextmail(access_token=session.fresh_access_token)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`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 `AsyncNextmail` the function may also be `async`. Pass `api_key` or `access_token`, not both. `Nextmail()` reads `NEXTMAIL_ACCESS_TOKEN` when you pass neither and `NEXTMAIL_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.
|
|
239
|
+
|
|
240
|
+
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:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from nextmail import NextmailApiError
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
client.domains.delete(domain_id)
|
|
247
|
+
except NextmailApiError as error:
|
|
248
|
+
if not error.is_step_up_required:
|
|
249
|
+
raise
|
|
250
|
+
|
|
251
|
+
challenge = client.security.begin_step_up()
|
|
252
|
+
|
|
253
|
+
if challenge['method'] == 'email':
|
|
254
|
+
prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: '
|
|
255
|
+
else:
|
|
256
|
+
prompt = 'Enter the code from your authenticator app, or a backup code: '
|
|
257
|
+
|
|
258
|
+
client.security.verify_step_up({'code': input(prompt)})
|
|
259
|
+
client.domains.delete(domain_id)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
### Configuring
|
|
265
|
+
Pass keyword arguments when the defaults are not right:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
import os
|
|
269
|
+
|
|
270
|
+
import httpx
|
|
271
|
+
from nextmail import Nextmail
|
|
272
|
+
|
|
273
|
+
client = Nextmail(
|
|
274
|
+
os.environ['NEXTMAIL_API_KEY'],
|
|
275
|
+
base_url='https://api.nextmail.sh',
|
|
276
|
+
timeout=30,
|
|
277
|
+
max_retries=2,
|
|
278
|
+
http_client=httpx.Client(proxy='http://proxy.internal:3128', follow_redirects=True),
|
|
279
|
+
headers={'X-Team': 'billing'},
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The shipped `nextmail` client takes the same arguments through `init(...)`, called once at startup.
|
|
284
|
+
|
|
285
|
+
`base_url` also comes from `NEXTMAIL_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.
|
|
286
|
+
|
|
287
|
+
`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=`.
|
|
288
|
+
|
|
289
|
+
An endpoint no method wraps yet is one `client.raw.request()` away, with the client's credential, base URL, timeout and retry policy applied:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
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.
|
|
296
|
+
|
|
297
|
+
When a newer version is on PyPI the client says so once on a terminal. `NEXTMAIL_DISABLE_UPDATE_NOTICE=1` or `disable_update_notice=True` turns that off.
|