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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .mypy_cache/
8
+ .pytest_cache/
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
+ }
@@ -0,0 +1,3 @@
1
+ """The version of the package. It is the version of the API that the client was written for."""
2
+
3
+ __version__ = "0.3.0"