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.
Files changed (151) hide show
  1. nextmail-0.0.1/.gitignore +7 -0
  2. nextmail-0.0.1/CHANGELOG.md +283 -0
  3. nextmail-0.0.1/PKG-INFO +297 -0
  4. nextmail-0.0.1/README.md +265 -0
  5. nextmail-0.0.1/pyproject.toml +144 -0
  6. nextmail-0.0.1/src/nextmail/__init__.py +566 -0
  7. nextmail-0.0.1/src/nextmail/_async/__init__.py +0 -0
  8. nextmail-0.0.1/src/nextmail/_async/client.py +467 -0
  9. nextmail-0.0.1/src/nextmail/_async/pagination.py +104 -0
  10. nextmail-0.0.1/src/nextmail/_async/resources/__init__.py +0 -0
  11. nextmail-0.0.1/src/nextmail/_async/resources/account.py +577 -0
  12. nextmail-0.0.1/src/nextmail/_async/resources/addresses.py +89 -0
  13. nextmail-0.0.1/src/nextmail/_async/resources/analytics.py +102 -0
  14. nextmail-0.0.1/src/nextmail/_async/resources/app_host.py +72 -0
  15. nextmail-0.0.1/src/nextmail/_async/resources/audiences.py +386 -0
  16. nextmail-0.0.1/src/nextmail/_async/resources/automations.py +567 -0
  17. nextmail-0.0.1/src/nextmail/_async/resources/billing.py +283 -0
  18. nextmail-0.0.1/src/nextmail/_async/resources/bookings.py +432 -0
  19. nextmail-0.0.1/src/nextmail/_async/resources/branding.py +90 -0
  20. nextmail-0.0.1/src/nextmail/_async/resources/broadcasts.py +424 -0
  21. nextmail-0.0.1/src/nextmail/_async/resources/calendar.py +541 -0
  22. nextmail-0.0.1/src/nextmail/_async/resources/chats.py +277 -0
  23. nextmail-0.0.1/src/nextmail/_async/resources/connected_mailboxes.py +129 -0
  24. nextmail-0.0.1/src/nextmail/_async/resources/connectors.py +370 -0
  25. nextmail-0.0.1/src/nextmail/_async/resources/contacts.py +868 -0
  26. nextmail-0.0.1/src/nextmail/_async/resources/dns_connections.py +74 -0
  27. nextmail-0.0.1/src/nextmail/_async/resources/domains.py +1084 -0
  28. nextmail-0.0.1/src/nextmail/_async/resources/drafts.py +155 -0
  29. nextmail-0.0.1/src/nextmail/_async/resources/emails.py +516 -0
  30. nextmail-0.0.1/src/nextmail/_async/resources/encryption.py +68 -0
  31. nextmail-0.0.1/src/nextmail/_async/resources/events.py +87 -0
  32. nextmail-0.0.1/src/nextmail/_async/resources/exports.py +84 -0
  33. nextmail-0.0.1/src/nextmail/_async/resources/files.py +332 -0
  34. nextmail-0.0.1/src/nextmail/_async/resources/follow_ups.py +85 -0
  35. nextmail-0.0.1/src/nextmail/_async/resources/forms.py +481 -0
  36. nextmail-0.0.1/src/nextmail/_async/resources/imports.py +370 -0
  37. nextmail-0.0.1/src/nextmail/_async/resources/keys.py +518 -0
  38. nextmail-0.0.1/src/nextmail/_async/resources/knowledge.py +550 -0
  39. nextmail-0.0.1/src/nextmail/_async/resources/labels.py +172 -0
  40. nextmail-0.0.1/src/nextmail/_async/resources/languages.py +32 -0
  41. nextmail-0.0.1/src/nextmail/_async/resources/me.py +224 -0
  42. nextmail-0.0.1/src/nextmail/_async/resources/members.py +386 -0
  43. nextmail-0.0.1/src/nextmail/_async/resources/out_of_office.py +69 -0
  44. nextmail-0.0.1/src/nextmail/_async/resources/provider_imports.py +168 -0
  45. nextmail-0.0.1/src/nextmail/_async/resources/roles.py +156 -0
  46. nextmail-0.0.1/src/nextmail/_async/resources/rules.py +299 -0
  47. nextmail-0.0.1/src/nextmail/_async/resources/security.py +154 -0
  48. nextmail-0.0.1/src/nextmail/_async/resources/segments.py +115 -0
  49. nextmail-0.0.1/src/nextmail/_async/resources/senders.py +59 -0
  50. nextmail-0.0.1/src/nextmail/_async/resources/sending.py +74 -0
  51. nextmail-0.0.1/src/nextmail/_async/resources/settings.py +48 -0
  52. nextmail-0.0.1/src/nextmail/_async/resources/signatures.py +178 -0
  53. nextmail-0.0.1/src/nextmail/_async/resources/subscriptions.py +195 -0
  54. nextmail-0.0.1/src/nextmail/_async/resources/support.py +31 -0
  55. nextmail-0.0.1/src/nextmail/_async/resources/suppressions.py +203 -0
  56. nextmail-0.0.1/src/nextmail/_async/resources/tasks.py +439 -0
  57. nextmail-0.0.1/src/nextmail/_async/resources/temp_mail.py +230 -0
  58. nextmail-0.0.1/src/nextmail/_async/resources/templates.py +635 -0
  59. nextmail-0.0.1/src/nextmail/_async/resources/threads.py +858 -0
  60. nextmail-0.0.1/src/nextmail/_async/resources/tools.py +138 -0
  61. nextmail-0.0.1/src/nextmail/_async/resources/tracking.py +300 -0
  62. nextmail-0.0.1/src/nextmail/_async/resources/webhooks.py +681 -0
  63. nextmail-0.0.1/src/nextmail/_async/resources/workspaces.py +139 -0
  64. nextmail-0.0.1/src/nextmail/_async/transport.py +252 -0
  65. nextmail-0.0.1/src/nextmail/_build.py +1 -0
  66. nextmail-0.0.1/src/nextmail/_core/__init__.py +0 -0
  67. nextmail-0.0.1/src/nextmail/_core/credentials.py +65 -0
  68. nextmail-0.0.1/src/nextmail/_core/crop_query.py +59 -0
  69. nextmail-0.0.1/src/nextmail/_core/environment.py +34 -0
  70. nextmail-0.0.1/src/nextmail/_core/errors.py +138 -0
  71. nextmail-0.0.1/src/nextmail/_core/hosts.py +234 -0
  72. nextmail-0.0.1/src/nextmail/_core/log_query.py +37 -0
  73. nextmail-0.0.1/src/nextmail/_core/namespace.py +32 -0
  74. nextmail-0.0.1/src/nextmail/_core/request_path.py +39 -0
  75. nextmail-0.0.1/src/nextmail/_core/response.py +201 -0
  76. nextmail-0.0.1/src/nextmail/_core/retry.py +50 -0
  77. nextmail-0.0.1/src/nextmail/_core/sentinel.py +16 -0
  78. nextmail-0.0.1/src/nextmail/_core/timeouts.py +17 -0
  79. nextmail-0.0.1/src/nextmail/_core/update_notice.py +89 -0
  80. nextmail-0.0.1/src/nextmail/_core/uploads.py +17 -0
  81. nextmail-0.0.1/src/nextmail/_core/wire.py +152 -0
  82. nextmail-0.0.1/src/nextmail/_default.py +76 -0
  83. nextmail-0.0.1/src/nextmail/_helpers/__init__.py +0 -0
  84. nextmail-0.0.1/src/nextmail/_helpers/languages.py +59 -0
  85. nextmail-0.0.1/src/nextmail/_helpers/messages.py +13 -0
  86. nextmail-0.0.1/src/nextmail/_helpers/webhooks.py +115 -0
  87. nextmail-0.0.1/src/nextmail/_sync/__init__.py +0 -0
  88. nextmail-0.0.1/src/nextmail/_sync/client.py +467 -0
  89. nextmail-0.0.1/src/nextmail/_sync/pagination.py +104 -0
  90. nextmail-0.0.1/src/nextmail/_sync/resources/__init__.py +0 -0
  91. nextmail-0.0.1/src/nextmail/_sync/resources/account.py +577 -0
  92. nextmail-0.0.1/src/nextmail/_sync/resources/addresses.py +87 -0
  93. nextmail-0.0.1/src/nextmail/_sync/resources/analytics.py +102 -0
  94. nextmail-0.0.1/src/nextmail/_sync/resources/app_host.py +72 -0
  95. nextmail-0.0.1/src/nextmail/_sync/resources/audiences.py +386 -0
  96. nextmail-0.0.1/src/nextmail/_sync/resources/automations.py +567 -0
  97. nextmail-0.0.1/src/nextmail/_sync/resources/billing.py +283 -0
  98. nextmail-0.0.1/src/nextmail/_sync/resources/bookings.py +432 -0
  99. nextmail-0.0.1/src/nextmail/_sync/resources/branding.py +90 -0
  100. nextmail-0.0.1/src/nextmail/_sync/resources/broadcasts.py +424 -0
  101. nextmail-0.0.1/src/nextmail/_sync/resources/calendar.py +541 -0
  102. nextmail-0.0.1/src/nextmail/_sync/resources/chats.py +277 -0
  103. nextmail-0.0.1/src/nextmail/_sync/resources/connected_mailboxes.py +129 -0
  104. nextmail-0.0.1/src/nextmail/_sync/resources/connectors.py +370 -0
  105. nextmail-0.0.1/src/nextmail/_sync/resources/contacts.py +868 -0
  106. nextmail-0.0.1/src/nextmail/_sync/resources/dns_connections.py +74 -0
  107. nextmail-0.0.1/src/nextmail/_sync/resources/domains.py +1084 -0
  108. nextmail-0.0.1/src/nextmail/_sync/resources/drafts.py +155 -0
  109. nextmail-0.0.1/src/nextmail/_sync/resources/emails.py +516 -0
  110. nextmail-0.0.1/src/nextmail/_sync/resources/encryption.py +68 -0
  111. nextmail-0.0.1/src/nextmail/_sync/resources/events.py +87 -0
  112. nextmail-0.0.1/src/nextmail/_sync/resources/exports.py +84 -0
  113. nextmail-0.0.1/src/nextmail/_sync/resources/files.py +332 -0
  114. nextmail-0.0.1/src/nextmail/_sync/resources/follow_ups.py +85 -0
  115. nextmail-0.0.1/src/nextmail/_sync/resources/forms.py +481 -0
  116. nextmail-0.0.1/src/nextmail/_sync/resources/imports.py +370 -0
  117. nextmail-0.0.1/src/nextmail/_sync/resources/keys.py +518 -0
  118. nextmail-0.0.1/src/nextmail/_sync/resources/knowledge.py +550 -0
  119. nextmail-0.0.1/src/nextmail/_sync/resources/labels.py +172 -0
  120. nextmail-0.0.1/src/nextmail/_sync/resources/languages.py +32 -0
  121. nextmail-0.0.1/src/nextmail/_sync/resources/me.py +224 -0
  122. nextmail-0.0.1/src/nextmail/_sync/resources/members.py +386 -0
  123. nextmail-0.0.1/src/nextmail/_sync/resources/out_of_office.py +69 -0
  124. nextmail-0.0.1/src/nextmail/_sync/resources/provider_imports.py +168 -0
  125. nextmail-0.0.1/src/nextmail/_sync/resources/roles.py +156 -0
  126. nextmail-0.0.1/src/nextmail/_sync/resources/rules.py +299 -0
  127. nextmail-0.0.1/src/nextmail/_sync/resources/security.py +154 -0
  128. nextmail-0.0.1/src/nextmail/_sync/resources/segments.py +115 -0
  129. nextmail-0.0.1/src/nextmail/_sync/resources/senders.py +59 -0
  130. nextmail-0.0.1/src/nextmail/_sync/resources/sending.py +74 -0
  131. nextmail-0.0.1/src/nextmail/_sync/resources/settings.py +48 -0
  132. nextmail-0.0.1/src/nextmail/_sync/resources/signatures.py +178 -0
  133. nextmail-0.0.1/src/nextmail/_sync/resources/subscriptions.py +195 -0
  134. nextmail-0.0.1/src/nextmail/_sync/resources/support.py +31 -0
  135. nextmail-0.0.1/src/nextmail/_sync/resources/suppressions.py +203 -0
  136. nextmail-0.0.1/src/nextmail/_sync/resources/tasks.py +439 -0
  137. nextmail-0.0.1/src/nextmail/_sync/resources/temp_mail.py +230 -0
  138. nextmail-0.0.1/src/nextmail/_sync/resources/templates.py +635 -0
  139. nextmail-0.0.1/src/nextmail/_sync/resources/threads.py +858 -0
  140. nextmail-0.0.1/src/nextmail/_sync/resources/tools.py +138 -0
  141. nextmail-0.0.1/src/nextmail/_sync/resources/tracking.py +300 -0
  142. nextmail-0.0.1/src/nextmail/_sync/resources/webhooks.py +681 -0
  143. nextmail-0.0.1/src/nextmail/_sync/resources/workspaces.py +139 -0
  144. nextmail-0.0.1/src/nextmail/_sync/transport.py +258 -0
  145. nextmail-0.0.1/src/nextmail/_version.py +1 -0
  146. nextmail-0.0.1/src/nextmail/constants/__init__.py +4591 -0
  147. nextmail-0.0.1/src/nextmail/constants/client.py +79 -0
  148. nextmail-0.0.1/src/nextmail/py.typed +0 -0
  149. nextmail-0.0.1/src/nextmail/types/__init__.py +3359 -0
  150. nextmail-0.0.1/src/nextmail/types/__init__.pyi +11533 -0
  151. nextmail-0.0.1/src/nextmail/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,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`.
@@ -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.