sendora 2.0.2__tar.gz → 2.1.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.
- {sendora-2.0.2 → sendora-2.1.0}/CHANGELOG.md +6 -0
- {sendora-2.0.2 → sendora-2.1.0}/PKG-INFO +27 -3
- {sendora-2.0.2 → sendora-2.1.0}/README.md +26 -2
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_account.py +5 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_answers.py +1 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_version.py +1 -1
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/__init__.py +3 -0
- sendora-2.1.0/src/sendora/resources/tracking_domains.py +236 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/skills/sendora/SKILL.md +2 -2
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/types.py +128 -0
- {sendora-2.0.2 → sendora-2.1.0}/.gitignore +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/LICENSE +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/pyproject.toml +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/__init__.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_client.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_errors.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_json.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_keys.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_model.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_options.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_pages.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_retry.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_transport.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_webhooks.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_wire.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/py.typed +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/broadcasts.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/domains.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/email.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/erasures.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/inbound.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/inbound_domains.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/messages.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/server.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/servers.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/streams.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/suppressions.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/tokens.py +0 -0
- {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/webhooks.py +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
- `SendoraAccount.tracking_domains` and `AsyncSendoraAccount.tracking_domains`
|
|
6
|
+
add, list, read, check and remove tracking domains, with the models
|
|
7
|
+
`TrackingDomain` and `TrackingDomainList` and five new error codes.
|
|
8
|
+
|
|
3
9
|
## 2.0.2
|
|
4
10
|
|
|
5
11
|
- The README says what `payment_required` means in each of its four cases:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sendora
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The Python client for Sendora's email API.
|
|
5
5
|
Project-URL: Homepage, https://sendora.se/docs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,7 +35,7 @@ Two kinds of key, two clients. `Sendora` takes a server key (`sk_…`) and
|
|
|
35
35
|
does everything inside one server: sending, broadcasts, the log, received
|
|
36
36
|
mail, streams, suppressions, tokens, webhooks and the server's own limits.
|
|
37
37
|
`SendoraAccount` takes an account key (`ak_…`) and manages the account:
|
|
38
|
-
its servers, their keys
|
|
38
|
+
its servers, their keys, its sending and tracking domains. `AsyncSendora` and
|
|
39
39
|
`AsyncSendoraAccount` are the same clients for asyncio. Both keys are
|
|
40
40
|
secrets: keep them on the server and never ship them to a browser.
|
|
41
41
|
|
|
@@ -436,6 +436,25 @@ print(domain.return_path.type, domain.return_path.host, domain.return_path.value
|
|
|
436
436
|
print(domain.dkim.type, domain.dkim.host, domain.dkim.value)
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
+
A tracking domain is the host that tracked links and the open pixel use: a
|
|
440
|
+
subdomain of a verified sending domain, with one CNAME record to add. CAA
|
|
441
|
+
records above it must let Let's Encrypt issue its certificate. Tracking is
|
|
442
|
+
not open to accounts yet, so a host carries no mail until it is. A host
|
|
443
|
+
whose records are not right within 7 days goes.
|
|
444
|
+
|
|
445
|
+
| Call | Answers |
|
|
446
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------- |
|
|
447
|
+
| `account.tracking_domains.create(host=)` | `TrackingDomain` with `cname` to add |
|
|
448
|
+
| `account.tracking_domains.list()` | `TrackingDomainList` with `tracking_domains` |
|
|
449
|
+
| `account.tracking_domains.get(tracking_domain_id)` | One tracking domain with what the last check found |
|
|
450
|
+
| `account.tracking_domains.verify(tracking_domain_id)` | The tracking domain as it stands: `state`, `cname.check`, `caa.check` |
|
|
451
|
+
| `account.tracking_domains.delete(tracking_domain_id)` | `None`; new mail stops using it at once |
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
tracking = account.tracking_domains.create(host="links.example.se")
|
|
455
|
+
print(tracking.cname.type, tracking.cname.host, tracking.cname.value)
|
|
456
|
+
```
|
|
457
|
+
|
|
439
458
|
An erasure removes one person's data from every server of the account,
|
|
440
459
|
as a request under the right to erasure asks. The content of every
|
|
441
460
|
message to or from the address, sent and received, is deleted, and the
|
|
@@ -508,6 +527,11 @@ except SendoraError as error:
|
|
|
508
527
|
| `not_found` | 404 | No such id on this server, or no such suppressed address. |
|
|
509
528
|
| `domain_exists` | 409 | The account already has the domain; `existing_id` names it. |
|
|
510
529
|
| `domain_reserved` | 400 | The name is Sendora's own; no account can receive on it. |
|
|
530
|
+
| `tracking_domain_exists` | 409 | The account already has the host, or one on that sending domain; `existing_id` names it. |
|
|
531
|
+
| `tracking_domain_limit` | 409 | The account has as many tracking domains as it may. |
|
|
532
|
+
| `tracking_host_is_sending_domain` | 422 | The host is a sending domain itself; choose a subdomain. |
|
|
533
|
+
| `tracking_host_not_verified` | 422 | The host is not under a verified sending domain of the account. |
|
|
534
|
+
| `tracking_host_taken` | 409 | Another account holds the host. |
|
|
511
535
|
| `inbound_domain_exists` | 409 | The stream already has a domain (`existing_id` names it), or another account holds the name verified. |
|
|
512
536
|
| `webhook_exists` | 409 | The server already has a webhook for that URL; `existing_id` names it. |
|
|
513
537
|
| `stream_exists` | 409 | The server already has a stream with that name; `existing_id` names it. |
|
|
@@ -574,7 +598,7 @@ seconds of waiting in all. A longer `retry_after` is raised at once for
|
|
|
574
598
|
you to schedule. A call that creates something (`streams.create`,
|
|
575
599
|
`tokens.create`, `webhooks.create`, `webhooks.create_secret`,
|
|
576
600
|
`webhooks.replay`, `inbound_domains.create`, `servers.create`,
|
|
577
|
-
`servers.tokens.create`, `domains.create`) is repeated only after a
|
|
601
|
+
`servers.tokens.create`, `domains.create`, `tracking_domains.create`) is repeated only after a
|
|
578
602
|
`rate_limited` or `sending_disabled` answer, never after a lost connection
|
|
579
603
|
or a server error, so nothing is created twice. Nothing else is retried.
|
|
580
604
|
|
|
@@ -13,7 +13,7 @@ Two kinds of key, two clients. `Sendora` takes a server key (`sk_…`) and
|
|
|
13
13
|
does everything inside one server: sending, broadcasts, the log, received
|
|
14
14
|
mail, streams, suppressions, tokens, webhooks and the server's own limits.
|
|
15
15
|
`SendoraAccount` takes an account key (`ak_…`) and manages the account:
|
|
16
|
-
its servers, their keys
|
|
16
|
+
its servers, their keys, its sending and tracking domains. `AsyncSendora` and
|
|
17
17
|
`AsyncSendoraAccount` are the same clients for asyncio. Both keys are
|
|
18
18
|
secrets: keep them on the server and never ship them to a browser.
|
|
19
19
|
|
|
@@ -414,6 +414,25 @@ print(domain.return_path.type, domain.return_path.host, domain.return_path.value
|
|
|
414
414
|
print(domain.dkim.type, domain.dkim.host, domain.dkim.value)
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
+
A tracking domain is the host that tracked links and the open pixel use: a
|
|
418
|
+
subdomain of a verified sending domain, with one CNAME record to add. CAA
|
|
419
|
+
records above it must let Let's Encrypt issue its certificate. Tracking is
|
|
420
|
+
not open to accounts yet, so a host carries no mail until it is. A host
|
|
421
|
+
whose records are not right within 7 days goes.
|
|
422
|
+
|
|
423
|
+
| Call | Answers |
|
|
424
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------- |
|
|
425
|
+
| `account.tracking_domains.create(host=)` | `TrackingDomain` with `cname` to add |
|
|
426
|
+
| `account.tracking_domains.list()` | `TrackingDomainList` with `tracking_domains` |
|
|
427
|
+
| `account.tracking_domains.get(tracking_domain_id)` | One tracking domain with what the last check found |
|
|
428
|
+
| `account.tracking_domains.verify(tracking_domain_id)` | The tracking domain as it stands: `state`, `cname.check`, `caa.check` |
|
|
429
|
+
| `account.tracking_domains.delete(tracking_domain_id)` | `None`; new mail stops using it at once |
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
tracking = account.tracking_domains.create(host="links.example.se")
|
|
433
|
+
print(tracking.cname.type, tracking.cname.host, tracking.cname.value)
|
|
434
|
+
```
|
|
435
|
+
|
|
417
436
|
An erasure removes one person's data from every server of the account,
|
|
418
437
|
as a request under the right to erasure asks. The content of every
|
|
419
438
|
message to or from the address, sent and received, is deleted, and the
|
|
@@ -486,6 +505,11 @@ except SendoraError as error:
|
|
|
486
505
|
| `not_found` | 404 | No such id on this server, or no such suppressed address. |
|
|
487
506
|
| `domain_exists` | 409 | The account already has the domain; `existing_id` names it. |
|
|
488
507
|
| `domain_reserved` | 400 | The name is Sendora's own; no account can receive on it. |
|
|
508
|
+
| `tracking_domain_exists` | 409 | The account already has the host, or one on that sending domain; `existing_id` names it. |
|
|
509
|
+
| `tracking_domain_limit` | 409 | The account has as many tracking domains as it may. |
|
|
510
|
+
| `tracking_host_is_sending_domain` | 422 | The host is a sending domain itself; choose a subdomain. |
|
|
511
|
+
| `tracking_host_not_verified` | 422 | The host is not under a verified sending domain of the account. |
|
|
512
|
+
| `tracking_host_taken` | 409 | Another account holds the host. |
|
|
489
513
|
| `inbound_domain_exists` | 409 | The stream already has a domain (`existing_id` names it), or another account holds the name verified. |
|
|
490
514
|
| `webhook_exists` | 409 | The server already has a webhook for that URL; `existing_id` names it. |
|
|
491
515
|
| `stream_exists` | 409 | The server already has a stream with that name; `existing_id` names it. |
|
|
@@ -552,7 +576,7 @@ seconds of waiting in all. A longer `retry_after` is raised at once for
|
|
|
552
576
|
you to schedule. A call that creates something (`streams.create`,
|
|
553
577
|
`tokens.create`, `webhooks.create`, `webhooks.create_secret`,
|
|
554
578
|
`webhooks.replay`, `inbound_domains.create`, `servers.create`,
|
|
555
|
-
`servers.tokens.create`, `domains.create`) is repeated only after a
|
|
579
|
+
`servers.tokens.create`, `domains.create`, `tracking_domains.create`) is repeated only after a
|
|
556
580
|
`rate_limited` or `sending_disabled` answer, never after a lost connection
|
|
557
581
|
or a server error, so nothing is created twice. Nothing else is retried.
|
|
558
582
|
|
|
@@ -8,6 +8,7 @@ from ._transport import AsyncHttpClient, AsyncTransport, HttpClient, SyncTranspo
|
|
|
8
8
|
from .resources.domains import AsyncDomains, Domains
|
|
9
9
|
from .resources.erasures import AsyncErasures, Erasures
|
|
10
10
|
from .resources.servers import AsyncServers, Servers
|
|
11
|
+
from .resources.tracking_domains import AsyncTrackingDomains, TrackingDomains
|
|
11
12
|
|
|
12
13
|
_ACCOUNT_KEY = "SENDORA_ACCOUNT_TOKEN"
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ class SendoraAccount:
|
|
|
23
24
|
Attributes:
|
|
24
25
|
servers: The account's servers, and each server's keys.
|
|
25
26
|
domains: The account's sending domains.
|
|
27
|
+
tracking_domains: The hosts that tracked links and the open pixel use.
|
|
26
28
|
erasures: A person's data erased from every server of the account.
|
|
27
29
|
|
|
28
30
|
Example:
|
|
@@ -81,6 +83,7 @@ class SendoraAccount:
|
|
|
81
83
|
self._transport = transport
|
|
82
84
|
self.servers: Servers = Servers(transport)
|
|
83
85
|
self.domains: Domains = Domains(transport)
|
|
86
|
+
self.tracking_domains: TrackingDomains = TrackingDomains(transport)
|
|
84
87
|
self.erasures: Erasures = Erasures(transport)
|
|
85
88
|
|
|
86
89
|
def with_options(
|
|
@@ -131,6 +134,7 @@ class AsyncSendoraAccount:
|
|
|
131
134
|
Attributes:
|
|
132
135
|
servers: The account's servers, and each server's keys.
|
|
133
136
|
domains: The account's sending domains.
|
|
137
|
+
tracking_domains: The hosts that tracked links and the open pixel use.
|
|
134
138
|
erasures: A person's data erased from every server of the account.
|
|
135
139
|
|
|
136
140
|
Example:
|
|
@@ -188,6 +192,7 @@ class AsyncSendoraAccount:
|
|
|
188
192
|
self._transport = transport
|
|
189
193
|
self.servers: AsyncServers = AsyncServers(transport)
|
|
190
194
|
self.domains: AsyncDomains = AsyncDomains(transport)
|
|
195
|
+
self.tracking_domains: AsyncTrackingDomains = AsyncTrackingDomains(transport)
|
|
191
196
|
self.erasures: AsyncErasures = AsyncErasures(transport)
|
|
192
197
|
|
|
193
198
|
def with_options(
|
|
@@ -12,6 +12,7 @@ from .servers import AsyncServers, AsyncServerTokens, Servers, ServerTokens
|
|
|
12
12
|
from .streams import AsyncStreams, Streams
|
|
13
13
|
from .suppressions import AsyncSuppressions, Suppressions
|
|
14
14
|
from .tokens import AsyncTokens, Tokens
|
|
15
|
+
from .tracking_domains import AsyncTrackingDomains, TrackingDomains
|
|
15
16
|
from .webhooks import AsyncWebhooks, Webhooks
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
@@ -28,6 +29,7 @@ __all__ = [
|
|
|
28
29
|
"AsyncSuppressions",
|
|
29
30
|
"AsyncThisServer",
|
|
30
31
|
"AsyncTokens",
|
|
32
|
+
"AsyncTrackingDomains",
|
|
31
33
|
"AsyncWebhooks",
|
|
32
34
|
"Broadcasts",
|
|
33
35
|
"Domains",
|
|
@@ -42,5 +44,6 @@ __all__ = [
|
|
|
42
44
|
"Suppressions",
|
|
43
45
|
"ThisServer",
|
|
44
46
|
"Tokens",
|
|
47
|
+
"TrackingDomains",
|
|
45
48
|
"Webhooks",
|
|
46
49
|
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""The account's tracking domains, which tracked links and the open pixel use."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from sendora._transport import AsyncTransport, Call, SyncTransport
|
|
6
|
+
from sendora._wire import decode
|
|
7
|
+
from sendora.types import TrackingDomain, TrackingDomainList
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrackingDomains:
|
|
11
|
+
"""Adds, checks and removes the hosts that tracked links use."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, transport: SyncTransport) -> None:
|
|
14
|
+
"""Takes the client's transport."""
|
|
15
|
+
self._transport = transport
|
|
16
|
+
|
|
17
|
+
def create(self, *, host: str) -> TrackingDomain:
|
|
18
|
+
"""Claims a host under a verified sending domain and answers the record to add.
|
|
19
|
+
|
|
20
|
+
A host whose records are not right within 7 days goes. A create is
|
|
21
|
+
never repeated after a lost connection, which may have claimed it.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
host: The host, such as ``links.example.se``.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The tracking domain, with the CNAME record to add.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ConflictError: The account has the host or one on that sending
|
|
31
|
+
domain (``tracking_domain_exists``, with ``existing_id``),
|
|
32
|
+
another account holds it (``tracking_host_taken``), or the
|
|
33
|
+
account has no room (``tracking_domain_limit``).
|
|
34
|
+
UnprocessableEntityError: The host is not under a verified sending
|
|
35
|
+
domain, or is one itself.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> added = account.tracking_domains.create(host="links.example.se")
|
|
39
|
+
>>> print(added.cname.host, added.cname.value)
|
|
40
|
+
"""
|
|
41
|
+
return decode(TrackingDomain, self._transport.send(_create_call(host)))
|
|
42
|
+
|
|
43
|
+
def list(self) -> TrackingDomainList:
|
|
44
|
+
"""Every tracking domain of the account.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The tracking domains.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> listed = account.tracking_domains.list()
|
|
51
|
+
>>> print(len(listed.tracking_domains))
|
|
52
|
+
"""
|
|
53
|
+
return decode(TrackingDomainList, self._transport.send(_LIST))
|
|
54
|
+
|
|
55
|
+
def get(self, tracking_domain_id: str) -> TrackingDomain:
|
|
56
|
+
"""One tracking domain by its id, with what the last check found.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tracking_domain_id: The tracking domain's id.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The tracking domain.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> domain = account.tracking_domains.get(tracking_domain_id)
|
|
69
|
+
"""
|
|
70
|
+
call = _get_call(tracking_domain_id)
|
|
71
|
+
return decode(TrackingDomain, self._transport.send(call))
|
|
72
|
+
|
|
73
|
+
def verify(self, tracking_domain_id: str) -> TrackingDomain:
|
|
74
|
+
"""Looks the CNAME and the CAA records up now.
|
|
75
|
+
|
|
76
|
+
A ``dns_error`` changes nothing.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
tracking_domain_id: The tracking domain's id.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The tracking domain as it stands.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> checked = account.tracking_domains.verify(tracking_domain_id)
|
|
89
|
+
>>> print(checked.state, checked.cname.check, checked.caa.check)
|
|
90
|
+
"""
|
|
91
|
+
call = _verify_call(tracking_domain_id)
|
|
92
|
+
return decode(TrackingDomain, self._transport.send(call))
|
|
93
|
+
|
|
94
|
+
def delete(self, tracking_domain_id: str) -> None:
|
|
95
|
+
"""Removes the tracking domain; new mail stops using it at once.
|
|
96
|
+
|
|
97
|
+
Links in sent mail keep working while the CNAME record points at
|
|
98
|
+
Sendora. A delete repeated after a lost answer whose first attempt
|
|
99
|
+
took effect raises ``not_found``.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tracking_domain_id: The tracking domain's id.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
>>> account.tracking_domains.delete(tracking_domain_id)
|
|
109
|
+
"""
|
|
110
|
+
self._transport.send(_delete_call(tracking_domain_id))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AsyncTrackingDomains:
|
|
114
|
+
"""Adds, checks and removes the hosts that tracked links use."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
117
|
+
"""Takes the client's transport."""
|
|
118
|
+
self._transport = transport
|
|
119
|
+
|
|
120
|
+
async def create(self, *, host: str) -> TrackingDomain:
|
|
121
|
+
"""Claims a host under a verified sending domain and answers the record to add.
|
|
122
|
+
|
|
123
|
+
A host whose records are not right within 7 days goes. A create is
|
|
124
|
+
never repeated after a lost connection, which may have claimed it.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
host: The host, such as ``links.example.se``.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The tracking domain, with the CNAME record to add.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ConflictError: The account has the host or one on that sending
|
|
134
|
+
domain (``tracking_domain_exists``, with ``existing_id``),
|
|
135
|
+
another account holds it (``tracking_host_taken``), or the
|
|
136
|
+
account has no room (``tracking_domain_limit``).
|
|
137
|
+
UnprocessableEntityError: The host is not under a verified sending
|
|
138
|
+
domain, or is one itself.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> added = await account.tracking_domains.create(host="links.example.se")
|
|
142
|
+
>>> print(added.cname.host, added.cname.value)
|
|
143
|
+
"""
|
|
144
|
+
return decode(TrackingDomain, await self._transport.send(_create_call(host)))
|
|
145
|
+
|
|
146
|
+
async def list(self) -> TrackingDomainList:
|
|
147
|
+
"""Every tracking domain of the account.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The tracking domains.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> listed = await account.tracking_domains.list()
|
|
154
|
+
>>> print(len(listed.tracking_domains))
|
|
155
|
+
"""
|
|
156
|
+
return decode(TrackingDomainList, await self._transport.send(_LIST))
|
|
157
|
+
|
|
158
|
+
async def get(self, tracking_domain_id: str) -> TrackingDomain:
|
|
159
|
+
"""One tracking domain by its id, with what the last check found.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
tracking_domain_id: The tracking domain's id.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The tracking domain.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> domain = await account.tracking_domains.get(tracking_domain_id)
|
|
172
|
+
"""
|
|
173
|
+
call = _get_call(tracking_domain_id)
|
|
174
|
+
return decode(TrackingDomain, await self._transport.send(call))
|
|
175
|
+
|
|
176
|
+
async def verify(self, tracking_domain_id: str) -> TrackingDomain:
|
|
177
|
+
"""Looks the CNAME and the CAA records up now.
|
|
178
|
+
|
|
179
|
+
A ``dns_error`` changes nothing.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tracking_domain_id: The tracking domain's id.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The tracking domain as it stands.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> checked = await account.tracking_domains.verify(tracking_domain_id)
|
|
192
|
+
>>> print(checked.state, checked.cname.check, checked.caa.check)
|
|
193
|
+
"""
|
|
194
|
+
call = _verify_call(tracking_domain_id)
|
|
195
|
+
return decode(TrackingDomain, await self._transport.send(call))
|
|
196
|
+
|
|
197
|
+
async def delete(self, tracking_domain_id: str) -> None:
|
|
198
|
+
"""Removes the tracking domain; new mail stops using it at once.
|
|
199
|
+
|
|
200
|
+
Links in sent mail keep working while the CNAME record points at
|
|
201
|
+
Sendora. A delete repeated after a lost answer whose first attempt
|
|
202
|
+
took effect raises ``not_found``.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
tracking_domain_id: The tracking domain's id.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
NotFoundError: The account has no tracking domain with the id.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> await account.tracking_domains.delete(tracking_domain_id)
|
|
212
|
+
"""
|
|
213
|
+
await self._transport.send(_delete_call(tracking_domain_id))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_LIST = Call("GET", "/v1/tracking-domains", idempotent=True)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _create_call(host: str) -> Call:
|
|
220
|
+
return Call("POST", "/v1/tracking-domains", idempotent=False, body={"host": host})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _get_call(tracking_domain_id: str) -> Call:
|
|
224
|
+
return Call("GET", _path(tracking_domain_id), idempotent=True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _verify_call(tracking_domain_id: str) -> Call:
|
|
228
|
+
return Call("POST", f"{_path(tracking_domain_id)}/verify", idempotent=True)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _delete_call(tracking_domain_id: str) -> Call:
|
|
232
|
+
return Call("DELETE", _path(tracking_domain_id), idempotent=True, expect="none")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _path(tracking_domain_id: str) -> str:
|
|
236
|
+
return f"/v1/tracking-domains/{quote(tracking_domain_id, safe='')}"
|
|
@@ -16,8 +16,8 @@ and every event is in it.
|
|
|
16
16
|
`Sendora()`: `email`, `broadcasts`, `messages`, `inbound`,
|
|
17
17
|
`inbound_domains`, `server`, `streams`, `suppressions`, `tokens`,
|
|
18
18
|
`webhooks`, everything inside one server. An account key (`ak_…`)
|
|
19
|
-
drives `SendoraAccount()`: `servers`, `servers.tokens` and
|
|
20
|
-
and it never sends. A key of the wrong kind raises `wrong_token_kind`.
|
|
19
|
+
drives `SendoraAccount()`: `servers`, `servers.tokens`, `domains` and
|
|
20
|
+
`tracking_domains`, and it never sends. A key of the wrong kind raises `wrong_token_kind`.
|
|
21
21
|
`AsyncSendora` and `AsyncSendoraAccount` are the same clients for
|
|
22
22
|
asyncio; use them in async code and never call the regular ones there.
|
|
23
23
|
- Every key is a secret. Leave `token` out and the clients read
|
|
@@ -122,6 +122,14 @@ __all__ = [
|
|
|
122
122
|
"SuppressionReason",
|
|
123
123
|
"Token",
|
|
124
124
|
"TokenList",
|
|
125
|
+
"TrackingCaaCheck",
|
|
126
|
+
"TrackingCaaCheckResult",
|
|
127
|
+
"TrackingCaaRecord",
|
|
128
|
+
"TrackingCnameCheck",
|
|
129
|
+
"TrackingCnameRecord",
|
|
130
|
+
"TrackingDomain",
|
|
131
|
+
"TrackingDomainList",
|
|
132
|
+
"TrackingDomainState",
|
|
125
133
|
"TxtRecord",
|
|
126
134
|
"UnknownEvent",
|
|
127
135
|
"UnsubscribedDetails",
|
|
@@ -175,6 +183,11 @@ SendoraErrorCode = Literal[
|
|
|
175
183
|
"domain_exists",
|
|
176
184
|
"domain_reserved",
|
|
177
185
|
"inbound_domain_exists",
|
|
186
|
+
"tracking_domain_exists",
|
|
187
|
+
"tracking_domain_limit",
|
|
188
|
+
"tracking_host_is_sending_domain",
|
|
189
|
+
"tracking_host_not_verified",
|
|
190
|
+
"tracking_host_taken",
|
|
178
191
|
"webhook_exists",
|
|
179
192
|
"stream_exists",
|
|
180
193
|
"inbound_stream_exists",
|
|
@@ -1574,6 +1587,121 @@ class DomainList(Answer):
|
|
|
1574
1587
|
domains: list[SendingDomain]
|
|
1575
1588
|
|
|
1576
1589
|
|
|
1590
|
+
TrackingDomainState = Literal[
|
|
1591
|
+
"pending",
|
|
1592
|
+
"waiting_for_certificate",
|
|
1593
|
+
"ready",
|
|
1594
|
+
"failing",
|
|
1595
|
+
"stopped",
|
|
1596
|
+
"sending_domain_lost",
|
|
1597
|
+
]
|
|
1598
|
+
TrackingCnameCheck = Literal["ok", "missing", "mismatch", "proxied", "dns_error"]
|
|
1599
|
+
TrackingCaaCheck = Literal[
|
|
1600
|
+
"ok", "no_lets_encrypt", "other_account", "no_http_01", "dns_error"
|
|
1601
|
+
]
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
@model
|
|
1605
|
+
class TrackingCnameRecord(Answer):
|
|
1606
|
+
"""The CNAME record a tracking domain needs, and what the last check found.
|
|
1607
|
+
|
|
1608
|
+
Attributes:
|
|
1609
|
+
type: ``CNAME``.
|
|
1610
|
+
host: The name to create it under.
|
|
1611
|
+
value: The value it must hold.
|
|
1612
|
+
check: What the last check found; ``None`` before the first.
|
|
1613
|
+
``mismatch`` is a record that points elsewhere, an A record
|
|
1614
|
+
included. ``proxied`` is a proxy in front of the name.
|
|
1615
|
+
``dns_error`` is a resolver failure and changes nothing.
|
|
1616
|
+
"""
|
|
1617
|
+
|
|
1618
|
+
type: Literal["CNAME"]
|
|
1619
|
+
host: str
|
|
1620
|
+
value: str
|
|
1621
|
+
check: TrackingCnameCheck | None
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
@model
|
|
1625
|
+
class TrackingCaaRecord(Answer):
|
|
1626
|
+
"""The CAA record that lets a tracking domain's certificate be issued.
|
|
1627
|
+
|
|
1628
|
+
Attributes:
|
|
1629
|
+
type: ``CAA``.
|
|
1630
|
+
host: The name that holds the CAA records in the way.
|
|
1631
|
+
value: The record to add beside them.
|
|
1632
|
+
"""
|
|
1633
|
+
|
|
1634
|
+
type: Literal["CAA"]
|
|
1635
|
+
host: str
|
|
1636
|
+
value: str
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
@model
|
|
1640
|
+
class TrackingCaaCheckResult(Answer):
|
|
1641
|
+
"""Whether CAA records let Let's Encrypt issue the certificate.
|
|
1642
|
+
|
|
1643
|
+
Attributes:
|
|
1644
|
+
check: What the last check found; ``None`` before the first.
|
|
1645
|
+
``dns_error`` is a resolver failure and changes nothing.
|
|
1646
|
+
record: What lets the certificate be issued; ``None`` unless the
|
|
1647
|
+
check found a problem.
|
|
1648
|
+
"""
|
|
1649
|
+
|
|
1650
|
+
check: TrackingCaaCheck | None
|
|
1651
|
+
record: TrackingCaaRecord | None
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
@model
|
|
1655
|
+
class TrackingDomain(Answer):
|
|
1656
|
+
"""A tracking domain, the record it needs and what the last check found.
|
|
1657
|
+
|
|
1658
|
+
Attributes:
|
|
1659
|
+
tracking_domain_id: The tracking domain's id.
|
|
1660
|
+
host: The host, lower-cased and IDNA-encoded.
|
|
1661
|
+
sending_domain_id: The sending domain whose new mail will use the
|
|
1662
|
+
host; ``None`` once that domain is deleted.
|
|
1663
|
+
state: ``pending``: the records have not been right yet.
|
|
1664
|
+
``waiting_for_certificate``: the records are right. ``ready``:
|
|
1665
|
+
new mail from the sending domain uses the host. ``failing``: a
|
|
1666
|
+
check found the records wrong, and new mail keeps using a host
|
|
1667
|
+
that was ready until 24 hours after ``failing_since``.
|
|
1668
|
+
``stopped``: those 24 hours have passed.
|
|
1669
|
+
``sending_domain_lost``: the sending domain is deleted or has
|
|
1670
|
+
lost its verification.
|
|
1671
|
+
created_at: When it was added.
|
|
1672
|
+
last_checked_at: When the records were last looked up.
|
|
1673
|
+
certificate_ready_at: When the certificate was proved to work.
|
|
1674
|
+
failing_since: When a check first found the records wrong on a host
|
|
1675
|
+
whose records have been right.
|
|
1676
|
+
expires_at: When a host whose records have never been right goes.
|
|
1677
|
+
cname: The CNAME record to add.
|
|
1678
|
+
caa: Whether CAA records let Let's Encrypt issue the certificate.
|
|
1679
|
+
"""
|
|
1680
|
+
|
|
1681
|
+
tracking_domain_id: str
|
|
1682
|
+
host: str
|
|
1683
|
+
sending_domain_id: str | None
|
|
1684
|
+
state: TrackingDomainState
|
|
1685
|
+
created_at: datetime
|
|
1686
|
+
last_checked_at: datetime | None
|
|
1687
|
+
certificate_ready_at: datetime | None
|
|
1688
|
+
failing_since: datetime | None
|
|
1689
|
+
expires_at: datetime | None
|
|
1690
|
+
cname: TrackingCnameRecord
|
|
1691
|
+
caa: TrackingCaaCheckResult
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
@model
|
|
1695
|
+
class TrackingDomainList(Answer):
|
|
1696
|
+
"""Every tracking domain of the account.
|
|
1697
|
+
|
|
1698
|
+
Attributes:
|
|
1699
|
+
tracking_domains: The tracking domains.
|
|
1700
|
+
"""
|
|
1701
|
+
|
|
1702
|
+
tracking_domains: list[TrackingDomain]
|
|
1703
|
+
|
|
1704
|
+
|
|
1577
1705
|
ErasureStatus = Literal["pending", "done"]
|
|
1578
1706
|
|
|
1579
1707
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|