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.
Files changed (39) hide show
  1. {sendora-2.0.2 → sendora-2.1.0}/CHANGELOG.md +6 -0
  2. {sendora-2.0.2 → sendora-2.1.0}/PKG-INFO +27 -3
  3. {sendora-2.0.2 → sendora-2.1.0}/README.md +26 -2
  4. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_account.py +5 -0
  5. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_answers.py +1 -0
  6. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_version.py +1 -1
  7. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/__init__.py +3 -0
  8. sendora-2.1.0/src/sendora/resources/tracking_domains.py +236 -0
  9. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/skills/sendora/SKILL.md +2 -2
  10. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/types.py +128 -0
  11. {sendora-2.0.2 → sendora-2.1.0}/.gitignore +0 -0
  12. {sendora-2.0.2 → sendora-2.1.0}/LICENSE +0 -0
  13. {sendora-2.0.2 → sendora-2.1.0}/pyproject.toml +0 -0
  14. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/__init__.py +0 -0
  15. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_client.py +0 -0
  16. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_errors.py +0 -0
  17. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_json.py +0 -0
  18. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_keys.py +0 -0
  19. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_model.py +0 -0
  20. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_options.py +0 -0
  21. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_pages.py +0 -0
  22. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_retry.py +0 -0
  23. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_transport.py +0 -0
  24. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_webhooks.py +0 -0
  25. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/_wire.py +0 -0
  26. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/py.typed +0 -0
  27. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/broadcasts.py +0 -0
  28. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/domains.py +0 -0
  29. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/email.py +0 -0
  30. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/erasures.py +0 -0
  31. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/inbound.py +0 -0
  32. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/inbound_domains.py +0 -0
  33. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/messages.py +0 -0
  34. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/server.py +0 -0
  35. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/servers.py +0 -0
  36. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/streams.py +0 -0
  37. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/suppressions.py +0 -0
  38. {sendora-2.0.2 → sendora-2.1.0}/src/sendora/resources/tokens.py +0 -0
  39. {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.2
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 and its sending domains. `AsyncSendora` and
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 and its sending domains. `AsyncSendora` and
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(
@@ -246,6 +246,7 @@ _EXISTING_IDS = {
246
246
  "stream_exists": "streamId",
247
247
  "inbound_stream_exists": "streamId",
248
248
  "inbound_domain_exists": "inboundDomainId",
249
+ "tracking_domain_exists": "trackingDomainId",
249
250
  }
250
251
 
251
252
 
@@ -1,3 +1,3 @@
1
1
  """The package's version: the build reads it, and every request names it."""
2
2
 
3
- __version__ = "2.0.2"
3
+ __version__ = "2.1.0"
@@ -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 `domains`,
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