mxgate 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mxgate-0.3.0/.gitignore +8 -0
- mxgate-0.3.0/LICENSE +21 -0
- mxgate-0.3.0/PKG-INFO +239 -0
- mxgate-0.3.0/README.md +213 -0
- mxgate-0.3.0/pyproject.toml +52 -0
- mxgate-0.3.0/src/mxgate/__init__.py +13 -0
- mxgate-0.3.0/src/mxgate/_operations.py +45 -0
- mxgate-0.3.0/src/mxgate/_version.py +3 -0
- mxgate-0.3.0/src/mxgate/client.py +533 -0
- mxgate-0.3.0/src/mxgate/py.typed +0 -0
- mxgate-0.3.0/src/mxgate/types.py +547 -0
- mxgate-0.3.0/src/mxgate/webhooks.py +53 -0
mxgate-0.3.0/.gitignore
ADDED
mxgate-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andriy Chemerynskiy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mxgate-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mxgate
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python client for mxgate: email forwarding and send-as for a domain you own.
|
|
5
|
+
Project-URL: Homepage, https://mxgate.io
|
|
6
|
+
Project-URL: Documentation, https://mxgate.io/docs/sdks
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ai-agent,api-client,dkim,dns,email,email-forwarding,mx,send-as,smtp,webhook
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# mxgate for Python
|
|
28
|
+
|
|
29
|
+
Python client for [mxgate](https://mxgate.io): email forwarding and send-as for a domain you own.
|
|
30
|
+
|
|
31
|
+
mxgate receives mail for your domain and forwards it to mailboxes that you confirm. It also sends mail as addresses at your domain. This package calls the HTTP API at `https://api.mxgate.io`.
|
|
32
|
+
|
|
33
|
+
Python 3.9 or newer, no dependencies.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install mxgate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from mxgate import Mxgate
|
|
45
|
+
|
|
46
|
+
mx = Mxgate() # the key comes from MXGATE_API_KEY
|
|
47
|
+
added = mx.add_domain("example.com")
|
|
48
|
+
for record in added["domain"]["records"]:
|
|
49
|
+
print(record["type"], record["name"], record["value"])
|
|
50
|
+
# Publish the records at your DNS host. Then:
|
|
51
|
+
if mx.verify_domain("example.com")["verified"]:
|
|
52
|
+
mx.set_alias("*@example.com", "you@gmail.com")
|
|
53
|
+
mx.send("Ann <hello@example.com>", "client@example.org", "Hello", "Sent with mxgate.")
|
|
54
|
+
for entry in mx.logs(domain="example.com", limit=10)["entries"]:
|
|
55
|
+
print(entry["received_at"], entry["status"], entry["recipient"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
DNS changes can take some minutes. Call `verify_domain` again until `verified` is true.
|
|
59
|
+
|
|
60
|
+
`send` needs the DKIM record of the domain. `mx.send_as("example.com")["send_as"]["ready"]` is true when the domain can send.
|
|
61
|
+
|
|
62
|
+
No account yet? `Mxgate().signup("you@gmail.com")` returns the key in `api_key`. The key is shown one time. Store it.
|
|
63
|
+
|
|
64
|
+
Each method returns the JSON answer of the API as a `dict`. The types in `mxgate.types` are `TypedDict` definitions, so an editor shows the fields.
|
|
65
|
+
|
|
66
|
+
## Where the key is read from
|
|
67
|
+
|
|
68
|
+
The client uses the first key that it finds:
|
|
69
|
+
|
|
70
|
+
1. The `api_key` argument: `Mxgate("mxg_...")`.
|
|
71
|
+
2. The environment variable `MXGATE_API_KEY`.
|
|
72
|
+
3. The field `api_key` in `~/.config/mxgate/config.json`. The `mxgate` CLI writes this file. If `XDG_CONFIG_HOME` is set, the file is `$XDG_CONFIG_HOME/mxgate/config.json`.
|
|
73
|
+
|
|
74
|
+
The base URL is `https://api.mxgate.io`. To change it, give `api_url` to `Mxgate()`, set `MXGATE_API_URL`, or set `api_url` in the config file.
|
|
75
|
+
|
|
76
|
+
`Mxgate(timeout=10)` sets the time limit of one call in seconds. The default is 30.
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
An error answer of the API raises `MxgateError`. A method never returns an error body.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import time
|
|
84
|
+
from mxgate import Mxgate, MxgateError
|
|
85
|
+
|
|
86
|
+
mx = Mxgate()
|
|
87
|
+
try:
|
|
88
|
+
mx.add_domain("example.com")
|
|
89
|
+
except MxgateError as err:
|
|
90
|
+
if err.code == "rate_limited":
|
|
91
|
+
time.sleep(err.retry_after_seconds or 60)
|
|
92
|
+
elif err.code == "plan_limit":
|
|
93
|
+
print(err.message, err.upgrade_url)
|
|
94
|
+
else:
|
|
95
|
+
print(err.status, err.code, err.message)
|
|
96
|
+
print("Next step:", err.fix)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Attribute | Meaning |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `status` | The HTTP status. 0 when no answer came. |
|
|
102
|
+
| `code` | The `error` field. Let your program switch on it. |
|
|
103
|
+
| `message` | One sentence for a person. Do not parse it. |
|
|
104
|
+
| `fix` | The next step, or `None`. Most errors have it. |
|
|
105
|
+
| `retry_after_seconds` | Only with `rate_limited`: how long to wait. |
|
|
106
|
+
| `upgrade_url` | Only with `plan_limit`: the page with the plans. |
|
|
107
|
+
| `body` | The full error body as a `dict`. |
|
|
108
|
+
|
|
109
|
+
`str(err)` is `"<code>: <message>"`, then `Fix: <fix>` when there is a fix.
|
|
110
|
+
|
|
111
|
+
The client adds three codes of its own:
|
|
112
|
+
|
|
113
|
+
- `network` (status 0): no answer came. The host is wrong, the network is down, or the time limit ended.
|
|
114
|
+
- `no_api_key` (status 401): the client found no key. It made no call.
|
|
115
|
+
- `bad_response`: the answer was not a JSON object, or the API URL redirects.
|
|
116
|
+
|
|
117
|
+
An address for `set_alias` or `remove_alias` that is not an alias address raises `ValueError`. The client makes no call.
|
|
118
|
+
|
|
119
|
+
The full list of codes is at https://mxgate.io/docs/errors.
|
|
120
|
+
|
|
121
|
+
## Check a webhook call
|
|
122
|
+
|
|
123
|
+
mxgate signs each webhook call. The header `mxgate-signature` is `t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `"<t>.<raw body>"` with your `webhook_secret`.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import json
|
|
127
|
+
from mxgate import verify_webhook
|
|
128
|
+
|
|
129
|
+
def handle(headers, raw_body: bytes) -> int:
|
|
130
|
+
if not verify_webhook(WEBHOOK_SECRET, headers.get("mxgate-signature", ""), raw_body):
|
|
131
|
+
return 400
|
|
132
|
+
event = json.loads(raw_body)
|
|
133
|
+
print(event["type"], event["data"])
|
|
134
|
+
return 200
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Give the body as the bytes that came, before a JSON parser changes them. The function returns `False` when the signature is wrong or when `t` is more than 300 seconds old. `tolerance_seconds` changes this limit.
|
|
138
|
+
|
|
139
|
+
To get a secret, set a webhook: `mx.set_notifications(webhook_url="https://example.com/hook")`. That answer shows `webhook_secret` in full. Later answers show only its first characters.
|
|
140
|
+
|
|
141
|
+
## Methods
|
|
142
|
+
|
|
143
|
+
| Method | API call | What it does |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `signup(email)` | `POST /v1/signup` | Creates an account. Returns the key one time. No key necessary. |
|
|
146
|
+
| `account()` | `GET /v1/account` | The plan, the limits, the usage and the subscription. |
|
|
147
|
+
| `keys()` | `GET /v1/keys` | The keys that work now. Only the first 12 characters of each. |
|
|
148
|
+
| `recover_key(email)` | `POST /v1/keys/recover` | Mails a recovery token for a lost key. No key necessary. |
|
|
149
|
+
| `confirm_recovery(token)` | `POST /v1/keys/recover/confirm` | Gives a new key for the token. Older keys stop. No key necessary. |
|
|
150
|
+
| `rotate_key()` | `POST /v1/keys/rotate` | Gives a new key. The key of the client and older keys stop. |
|
|
151
|
+
| `apps()` | `GET /v1/oauth/apps` | The apps that are connected to the account through OAuth. |
|
|
152
|
+
| `remove_app(app_id)` | `DELETE /v1/oauth/apps/{id}` | Disconnects an app. Its token stops working. |
|
|
153
|
+
| `checkout(interval="month")` | `POST /v1/billing/checkout` | A payment link for the paid plan. A person opens it. |
|
|
154
|
+
| `billing_portal()` | `POST /v1/billing/portal` | A link for a person: card, invoices, cancel. |
|
|
155
|
+
| `domains()` | `GET /v1/domains` | The domains of the account. |
|
|
156
|
+
| `add_domain(name)` | `POST /v1/domains` | Adds a domain. Returns the DNS records to publish. |
|
|
157
|
+
| `domain(name)` | `GET /v1/domains/{domain}` | One domain with its records and its last check. |
|
|
158
|
+
| `verify_domain(name)` | `POST /v1/domains/{domain}/verify` | Checks the DNS records now. |
|
|
159
|
+
| `remove_domain(name)` | `DELETE /v1/domains/{domain}` | Removes the domain, its aliases and its SMTP logins. |
|
|
160
|
+
| `aliases(domain)` | `GET /v1/domains/{domain}/aliases` | The aliases of the domain. |
|
|
161
|
+
| `set_alias(address, to)` | `PUT /v1/domains/{domain}/aliases/{alias}` | Creates or replaces an alias. `to` is one mailbox or a list. |
|
|
162
|
+
| `remove_alias(address)` | `DELETE /v1/domains/{domain}/aliases/{alias}` | Removes an alias. |
|
|
163
|
+
| `destinations()` | `GET /v1/destinations` | The mailboxes that the account can forward to. |
|
|
164
|
+
| `add_destination(email)` | `POST /v1/destinations` | Adds a mailbox. A confirmation link is mailed to it. |
|
|
165
|
+
| `send_as(domain)` | `GET /v1/domains/{domain}/smtp` | The send-as state, the SMTP host, the ports and the logins. |
|
|
166
|
+
| `create_smtp_credential(domain)` | `POST /v1/domains/{domain}/smtp` | Makes an SMTP login. The password is shown one time. |
|
|
167
|
+
| `remove_smtp_credential(domain, username)` | `DELETE /v1/domains/{domain}/smtp/{username}` | Removes an SMTP login. |
|
|
168
|
+
| `send(from_, to, subject, text, ...)` | `POST /v1/send` | Sends one plain-text message. |
|
|
169
|
+
| `logs(...)` | `GET /v1/logs` | One page of the message log, newest first. |
|
|
170
|
+
| `iter_logs(...)` | `GET /v1/logs` | Each row of the log. It gets the next pages for you. |
|
|
171
|
+
| `notifications()` | `GET /v1/notifications` | The notification settings and the 20 newest notifications. |
|
|
172
|
+
| `set_notifications(...)` | `PUT /v1/notifications` | Changes the email digest, the events and the webhook. |
|
|
173
|
+
| `test_webhook()` | `POST /v1/notifications/test` | Sends a signed test call to the webhook. |
|
|
174
|
+
| `status()` | `GET /status` | The health of the service for 30 days. No key necessary. |
|
|
175
|
+
| `index()` | `GET /` | The version and one line for each endpoint. No key necessary. |
|
|
176
|
+
| `login(email)` | `POST /v1/login` | Mails a sign-in code. For the website. No key necessary. |
|
|
177
|
+
| `confirm_login(email, code)` | `POST /v1/login/confirm` | Gives a session token for the code. No key necessary. |
|
|
178
|
+
| `logout()` | `POST /v1/logout` | Ends the session of the token that the client uses. |
|
|
179
|
+
| `google_login_url(state)` | `GET /v1/login/google` | Returns the URL of the sign-in with Google, for a browser. It makes no call. |
|
|
180
|
+
| `confirm_login_grant(grant)` | `POST /v1/login/grant` | Gives a session token for the grant of the sign-in with Google. No key necessary. |
|
|
181
|
+
|
|
182
|
+
### Alias addresses
|
|
183
|
+
|
|
184
|
+
`set_alias` and `remove_alias` take a full address. `hello@example.com` is the alias `hello` of `example.com`. `*@example.com` is the catch-all: it gets mail for each address of the domain that has no alias.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
mx.set_alias("hello@example.com", ["you@gmail.com", "client@example.org"])
|
|
188
|
+
mx.remove_alias("hello@example.com")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A destination that is new to the account gets a confirmation mail. mxgate forwards no mail to it before its owner confirms.
|
|
192
|
+
|
|
193
|
+
### Send a message
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
mx.send(
|
|
197
|
+
"Ann <hello@example.com>",
|
|
198
|
+
["client@example.org"],
|
|
199
|
+
"Re: Offer",
|
|
200
|
+
"Thank you for the offer.",
|
|
201
|
+
cc="you@gmail.com",
|
|
202
|
+
in_reply_to="4f2a9c1e@example.org",
|
|
203
|
+
idempotency_key="offer-reply-1",
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The optional fields are `cc`, `bcc`, `reply_to`, `in_reply_to`, `references`, `copy_to_owner` and `idempotency_key`. Give an `idempotency_key`: a second call with the same key sends nothing and returns the first answer. Then a retry after an error cannot send a second copy.
|
|
208
|
+
|
|
209
|
+
### Read the log
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
for entry in mx.iter_logs(domain="example.com", status="bounced"):
|
|
213
|
+
print(entry["recipient"], entry["explanation"])
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The filters of `logs` and `iter_logs` are `domain`, `direction` (`in` or `out`), `status`, `address`, `before` and `limit`. `logs` returns one page; `next_before` of the page is `before` of the next page. `iter_logs` reads all pages.
|
|
217
|
+
|
|
218
|
+
### Notifications
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
mx.set_notifications(email=True, events=["bounced", "expired"])
|
|
222
|
+
mx.set_notifications(webhook_url="https://example.com/hook")
|
|
223
|
+
mx.set_notifications(webhook_url=None) # removes the webhook
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
A setting that you do not give stays as it is.
|
|
227
|
+
|
|
228
|
+
## Tests
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
cd sdk/python
|
|
232
|
+
python3 -m unittest discover -s tests -v
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The tests use a local HTTP server. They make no call to mxgate.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
mxgate-0.3.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# mxgate for Python
|
|
2
|
+
|
|
3
|
+
Python client for [mxgate](https://mxgate.io): email forwarding and send-as for a domain you own.
|
|
4
|
+
|
|
5
|
+
mxgate receives mail for your domain and forwards it to mailboxes that you confirm. It also sends mail as addresses at your domain. This package calls the HTTP API at `https://api.mxgate.io`.
|
|
6
|
+
|
|
7
|
+
Python 3.9 or newer, no dependencies.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install mxgate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from mxgate import Mxgate
|
|
19
|
+
|
|
20
|
+
mx = Mxgate() # the key comes from MXGATE_API_KEY
|
|
21
|
+
added = mx.add_domain("example.com")
|
|
22
|
+
for record in added["domain"]["records"]:
|
|
23
|
+
print(record["type"], record["name"], record["value"])
|
|
24
|
+
# Publish the records at your DNS host. Then:
|
|
25
|
+
if mx.verify_domain("example.com")["verified"]:
|
|
26
|
+
mx.set_alias("*@example.com", "you@gmail.com")
|
|
27
|
+
mx.send("Ann <hello@example.com>", "client@example.org", "Hello", "Sent with mxgate.")
|
|
28
|
+
for entry in mx.logs(domain="example.com", limit=10)["entries"]:
|
|
29
|
+
print(entry["received_at"], entry["status"], entry["recipient"])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
DNS changes can take some minutes. Call `verify_domain` again until `verified` is true.
|
|
33
|
+
|
|
34
|
+
`send` needs the DKIM record of the domain. `mx.send_as("example.com")["send_as"]["ready"]` is true when the domain can send.
|
|
35
|
+
|
|
36
|
+
No account yet? `Mxgate().signup("you@gmail.com")` returns the key in `api_key`. The key is shown one time. Store it.
|
|
37
|
+
|
|
38
|
+
Each method returns the JSON answer of the API as a `dict`. The types in `mxgate.types` are `TypedDict` definitions, so an editor shows the fields.
|
|
39
|
+
|
|
40
|
+
## Where the key is read from
|
|
41
|
+
|
|
42
|
+
The client uses the first key that it finds:
|
|
43
|
+
|
|
44
|
+
1. The `api_key` argument: `Mxgate("mxg_...")`.
|
|
45
|
+
2. The environment variable `MXGATE_API_KEY`.
|
|
46
|
+
3. The field `api_key` in `~/.config/mxgate/config.json`. The `mxgate` CLI writes this file. If `XDG_CONFIG_HOME` is set, the file is `$XDG_CONFIG_HOME/mxgate/config.json`.
|
|
47
|
+
|
|
48
|
+
The base URL is `https://api.mxgate.io`. To change it, give `api_url` to `Mxgate()`, set `MXGATE_API_URL`, or set `api_url` in the config file.
|
|
49
|
+
|
|
50
|
+
`Mxgate(timeout=10)` sets the time limit of one call in seconds. The default is 30.
|
|
51
|
+
|
|
52
|
+
## Errors
|
|
53
|
+
|
|
54
|
+
An error answer of the API raises `MxgateError`. A method never returns an error body.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import time
|
|
58
|
+
from mxgate import Mxgate, MxgateError
|
|
59
|
+
|
|
60
|
+
mx = Mxgate()
|
|
61
|
+
try:
|
|
62
|
+
mx.add_domain("example.com")
|
|
63
|
+
except MxgateError as err:
|
|
64
|
+
if err.code == "rate_limited":
|
|
65
|
+
time.sleep(err.retry_after_seconds or 60)
|
|
66
|
+
elif err.code == "plan_limit":
|
|
67
|
+
print(err.message, err.upgrade_url)
|
|
68
|
+
else:
|
|
69
|
+
print(err.status, err.code, err.message)
|
|
70
|
+
print("Next step:", err.fix)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Attribute | Meaning |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `status` | The HTTP status. 0 when no answer came. |
|
|
76
|
+
| `code` | The `error` field. Let your program switch on it. |
|
|
77
|
+
| `message` | One sentence for a person. Do not parse it. |
|
|
78
|
+
| `fix` | The next step, or `None`. Most errors have it. |
|
|
79
|
+
| `retry_after_seconds` | Only with `rate_limited`: how long to wait. |
|
|
80
|
+
| `upgrade_url` | Only with `plan_limit`: the page with the plans. |
|
|
81
|
+
| `body` | The full error body as a `dict`. |
|
|
82
|
+
|
|
83
|
+
`str(err)` is `"<code>: <message>"`, then `Fix: <fix>` when there is a fix.
|
|
84
|
+
|
|
85
|
+
The client adds three codes of its own:
|
|
86
|
+
|
|
87
|
+
- `network` (status 0): no answer came. The host is wrong, the network is down, or the time limit ended.
|
|
88
|
+
- `no_api_key` (status 401): the client found no key. It made no call.
|
|
89
|
+
- `bad_response`: the answer was not a JSON object, or the API URL redirects.
|
|
90
|
+
|
|
91
|
+
An address for `set_alias` or `remove_alias` that is not an alias address raises `ValueError`. The client makes no call.
|
|
92
|
+
|
|
93
|
+
The full list of codes is at https://mxgate.io/docs/errors.
|
|
94
|
+
|
|
95
|
+
## Check a webhook call
|
|
96
|
+
|
|
97
|
+
mxgate signs each webhook call. The header `mxgate-signature` is `t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `"<t>.<raw body>"` with your `webhook_secret`.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import json
|
|
101
|
+
from mxgate import verify_webhook
|
|
102
|
+
|
|
103
|
+
def handle(headers, raw_body: bytes) -> int:
|
|
104
|
+
if not verify_webhook(WEBHOOK_SECRET, headers.get("mxgate-signature", ""), raw_body):
|
|
105
|
+
return 400
|
|
106
|
+
event = json.loads(raw_body)
|
|
107
|
+
print(event["type"], event["data"])
|
|
108
|
+
return 200
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Give the body as the bytes that came, before a JSON parser changes them. The function returns `False` when the signature is wrong or when `t` is more than 300 seconds old. `tolerance_seconds` changes this limit.
|
|
112
|
+
|
|
113
|
+
To get a secret, set a webhook: `mx.set_notifications(webhook_url="https://example.com/hook")`. That answer shows `webhook_secret` in full. Later answers show only its first characters.
|
|
114
|
+
|
|
115
|
+
## Methods
|
|
116
|
+
|
|
117
|
+
| Method | API call | What it does |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `signup(email)` | `POST /v1/signup` | Creates an account. Returns the key one time. No key necessary. |
|
|
120
|
+
| `account()` | `GET /v1/account` | The plan, the limits, the usage and the subscription. |
|
|
121
|
+
| `keys()` | `GET /v1/keys` | The keys that work now. Only the first 12 characters of each. |
|
|
122
|
+
| `recover_key(email)` | `POST /v1/keys/recover` | Mails a recovery token for a lost key. No key necessary. |
|
|
123
|
+
| `confirm_recovery(token)` | `POST /v1/keys/recover/confirm` | Gives a new key for the token. Older keys stop. No key necessary. |
|
|
124
|
+
| `rotate_key()` | `POST /v1/keys/rotate` | Gives a new key. The key of the client and older keys stop. |
|
|
125
|
+
| `apps()` | `GET /v1/oauth/apps` | The apps that are connected to the account through OAuth. |
|
|
126
|
+
| `remove_app(app_id)` | `DELETE /v1/oauth/apps/{id}` | Disconnects an app. Its token stops working. |
|
|
127
|
+
| `checkout(interval="month")` | `POST /v1/billing/checkout` | A payment link for the paid plan. A person opens it. |
|
|
128
|
+
| `billing_portal()` | `POST /v1/billing/portal` | A link for a person: card, invoices, cancel. |
|
|
129
|
+
| `domains()` | `GET /v1/domains` | The domains of the account. |
|
|
130
|
+
| `add_domain(name)` | `POST /v1/domains` | Adds a domain. Returns the DNS records to publish. |
|
|
131
|
+
| `domain(name)` | `GET /v1/domains/{domain}` | One domain with its records and its last check. |
|
|
132
|
+
| `verify_domain(name)` | `POST /v1/domains/{domain}/verify` | Checks the DNS records now. |
|
|
133
|
+
| `remove_domain(name)` | `DELETE /v1/domains/{domain}` | Removes the domain, its aliases and its SMTP logins. |
|
|
134
|
+
| `aliases(domain)` | `GET /v1/domains/{domain}/aliases` | The aliases of the domain. |
|
|
135
|
+
| `set_alias(address, to)` | `PUT /v1/domains/{domain}/aliases/{alias}` | Creates or replaces an alias. `to` is one mailbox or a list. |
|
|
136
|
+
| `remove_alias(address)` | `DELETE /v1/domains/{domain}/aliases/{alias}` | Removes an alias. |
|
|
137
|
+
| `destinations()` | `GET /v1/destinations` | The mailboxes that the account can forward to. |
|
|
138
|
+
| `add_destination(email)` | `POST /v1/destinations` | Adds a mailbox. A confirmation link is mailed to it. |
|
|
139
|
+
| `send_as(domain)` | `GET /v1/domains/{domain}/smtp` | The send-as state, the SMTP host, the ports and the logins. |
|
|
140
|
+
| `create_smtp_credential(domain)` | `POST /v1/domains/{domain}/smtp` | Makes an SMTP login. The password is shown one time. |
|
|
141
|
+
| `remove_smtp_credential(domain, username)` | `DELETE /v1/domains/{domain}/smtp/{username}` | Removes an SMTP login. |
|
|
142
|
+
| `send(from_, to, subject, text, ...)` | `POST /v1/send` | Sends one plain-text message. |
|
|
143
|
+
| `logs(...)` | `GET /v1/logs` | One page of the message log, newest first. |
|
|
144
|
+
| `iter_logs(...)` | `GET /v1/logs` | Each row of the log. It gets the next pages for you. |
|
|
145
|
+
| `notifications()` | `GET /v1/notifications` | The notification settings and the 20 newest notifications. |
|
|
146
|
+
| `set_notifications(...)` | `PUT /v1/notifications` | Changes the email digest, the events and the webhook. |
|
|
147
|
+
| `test_webhook()` | `POST /v1/notifications/test` | Sends a signed test call to the webhook. |
|
|
148
|
+
| `status()` | `GET /status` | The health of the service for 30 days. No key necessary. |
|
|
149
|
+
| `index()` | `GET /` | The version and one line for each endpoint. No key necessary. |
|
|
150
|
+
| `login(email)` | `POST /v1/login` | Mails a sign-in code. For the website. No key necessary. |
|
|
151
|
+
| `confirm_login(email, code)` | `POST /v1/login/confirm` | Gives a session token for the code. No key necessary. |
|
|
152
|
+
| `logout()` | `POST /v1/logout` | Ends the session of the token that the client uses. |
|
|
153
|
+
| `google_login_url(state)` | `GET /v1/login/google` | Returns the URL of the sign-in with Google, for a browser. It makes no call. |
|
|
154
|
+
| `confirm_login_grant(grant)` | `POST /v1/login/grant` | Gives a session token for the grant of the sign-in with Google. No key necessary. |
|
|
155
|
+
|
|
156
|
+
### Alias addresses
|
|
157
|
+
|
|
158
|
+
`set_alias` and `remove_alias` take a full address. `hello@example.com` is the alias `hello` of `example.com`. `*@example.com` is the catch-all: it gets mail for each address of the domain that has no alias.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
mx.set_alias("hello@example.com", ["you@gmail.com", "client@example.org"])
|
|
162
|
+
mx.remove_alias("hello@example.com")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A destination that is new to the account gets a confirmation mail. mxgate forwards no mail to it before its owner confirms.
|
|
166
|
+
|
|
167
|
+
### Send a message
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
mx.send(
|
|
171
|
+
"Ann <hello@example.com>",
|
|
172
|
+
["client@example.org"],
|
|
173
|
+
"Re: Offer",
|
|
174
|
+
"Thank you for the offer.",
|
|
175
|
+
cc="you@gmail.com",
|
|
176
|
+
in_reply_to="4f2a9c1e@example.org",
|
|
177
|
+
idempotency_key="offer-reply-1",
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The optional fields are `cc`, `bcc`, `reply_to`, `in_reply_to`, `references`, `copy_to_owner` and `idempotency_key`. Give an `idempotency_key`: a second call with the same key sends nothing and returns the first answer. Then a retry after an error cannot send a second copy.
|
|
182
|
+
|
|
183
|
+
### Read the log
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
for entry in mx.iter_logs(domain="example.com", status="bounced"):
|
|
187
|
+
print(entry["recipient"], entry["explanation"])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The filters of `logs` and `iter_logs` are `domain`, `direction` (`in` or `out`), `status`, `address`, `before` and `limit`. `logs` returns one page; `next_before` of the page is `before` of the next page. `iter_logs` reads all pages.
|
|
191
|
+
|
|
192
|
+
### Notifications
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
mx.set_notifications(email=True, events=["bounced", "expired"])
|
|
196
|
+
mx.set_notifications(webhook_url="https://example.com/hook")
|
|
197
|
+
mx.set_notifications(webhook_url=None) # removes the webhook
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A setting that you do not give stays as it is.
|
|
201
|
+
|
|
202
|
+
## Tests
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
cd sdk/python
|
|
206
|
+
python3 -m unittest discover -s tests -v
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The tests use a local HTTP server. They make no call to mxgate.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mxgate"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python client for mxgate: email forwarding and send-as for a domain you own."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = [
|
|
15
|
+
"email",
|
|
16
|
+
"email-forwarding",
|
|
17
|
+
"send-as",
|
|
18
|
+
"smtp",
|
|
19
|
+
"mx",
|
|
20
|
+
"dns",
|
|
21
|
+
"dkim",
|
|
22
|
+
"webhook",
|
|
23
|
+
"api-client",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Programming Language :: Python :: 3.9",
|
|
33
|
+
"Programming Language :: Python :: 3.10",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Programming Language :: Python :: 3.14",
|
|
38
|
+
"Topic :: Communications :: Email",
|
|
39
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://mxgate.io"
|
|
45
|
+
Documentation = "https://mxgate.io/docs/sdks"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/mxgate"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
# The tests are not in the sdist: tests/test_openapi.py needs site-src/openapi.json of the repository.
|
|
52
|
+
include = ["/src", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Python client for mxgate: email forwarding and send-as for a domain you own.
|
|
2
|
+
|
|
3
|
+
from mxgate import Mxgate, MxgateError
|
|
4
|
+
|
|
5
|
+
mx = Mxgate() # the key comes from MXGATE_API_KEY
|
|
6
|
+
print(mx.account()["plan"])
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .client import Mxgate, MxgateError
|
|
11
|
+
from .webhooks import verify_webhook
|
|
12
|
+
|
|
13
|
+
__all__ = ["Mxgate", "MxgateError", "__version__", "verify_webhook"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Each `operationId` of the OpenAPI document and the `Mxgate` method that calls it.
|
|
2
|
+
|
|
3
|
+
`tests/test_openapi.py` fails when the document has an operation that is not
|
|
4
|
+
here, or when a method that is named here does not exist.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
OPERATIONS: Dict[str, str] = {
|
|
10
|
+
"getIndex": "index",
|
|
11
|
+
"getStatus": "status",
|
|
12
|
+
"signUp": "signup",
|
|
13
|
+
"getAccount": "account",
|
|
14
|
+
"requestLogin": "login",
|
|
15
|
+
"confirmLogin": "confirm_login",
|
|
16
|
+
"startGoogleLogin": "google_login_url",
|
|
17
|
+
"confirmLoginGrant": "confirm_login_grant",
|
|
18
|
+
"logout": "logout",
|
|
19
|
+
"listKeys": "keys",
|
|
20
|
+
"recoverKey": "recover_key",
|
|
21
|
+
"confirmKeyRecovery": "confirm_recovery",
|
|
22
|
+
"rotateKey": "rotate_key",
|
|
23
|
+
"listConnectedApps": "apps",
|
|
24
|
+
"removeConnectedApp": "remove_app",
|
|
25
|
+
"createCheckout": "checkout",
|
|
26
|
+
"createPortalLink": "billing_portal",
|
|
27
|
+
"listDomains": "domains",
|
|
28
|
+
"addDomain": "add_domain",
|
|
29
|
+
"getDomain": "domain",
|
|
30
|
+
"deleteDomain": "remove_domain",
|
|
31
|
+
"verifyDomain": "verify_domain",
|
|
32
|
+
"listAliases": "aliases",
|
|
33
|
+
"setAlias": "set_alias",
|
|
34
|
+
"deleteAlias": "remove_alias",
|
|
35
|
+
"getSendAs": "send_as",
|
|
36
|
+
"createSmtpCredential": "create_smtp_credential",
|
|
37
|
+
"deleteSmtpCredential": "remove_smtp_credential",
|
|
38
|
+
"sendMessage": "send",
|
|
39
|
+
"listLogs": "logs",
|
|
40
|
+
"getNotifications": "notifications",
|
|
41
|
+
"setNotifications": "set_notifications",
|
|
42
|
+
"testWebhook": "test_webhook",
|
|
43
|
+
"listDestinations": "destinations",
|
|
44
|
+
"addDestination": "add_destination",
|
|
45
|
+
}
|