sendora 2.1.0__tar.gz → 2.2.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.1.0 → sendora-2.2.0}/CHANGELOG.md +10 -0
- {sendora-2.1.0 → sendora-2.2.0}/PKG-INFO +22 -17
- {sendora-2.1.0 → sendora-2.2.0}/README.md +21 -16
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_version.py +1 -1
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/broadcasts.py +15 -7
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/messages.py +2 -2
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/types.py +184 -2
- {sendora-2.1.0 → sendora-2.2.0}/.gitignore +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/LICENSE +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/pyproject.toml +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/__init__.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_account.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_answers.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_client.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_errors.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_json.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_keys.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_model.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_options.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_pages.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_retry.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_transport.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_webhooks.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/_wire.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/py.typed +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/__init__.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/domains.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/email.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/erasures.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/inbound.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/inbound_domains.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/server.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/servers.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/streams.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/suppressions.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/tokens.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/tracking_domains.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/resources/webhooks.py +0 -0
- {sendora-2.1.0 → sendora-2.2.0}/src/sendora/skills/sendora/SKILL.md +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
- Each `MessageRecipient` has `tracked`, `opens`, `clicks`, `first_open_at`
|
|
6
|
+
and `first_human_open_at`.
|
|
7
|
+
- `messages.get` returns `tracking`, with `recording_opens`,
|
|
8
|
+
`recording_clicks` and `skipped`. Its timeline holds `Open` and `Click`
|
|
9
|
+
events, each with its `tracking`.
|
|
10
|
+
- `broadcasts.get` returns `BroadcastDetail`, a `Broadcast` with `opened`,
|
|
11
|
+
`opened_by_people`, `clicked`, `clicked_by_people` and `links`.
|
|
12
|
+
|
|
3
13
|
## 2.1.0
|
|
4
14
|
|
|
5
15
|
- `SendoraAccount.tracking_domains` and `AsyncSendoraAccount.tracking_domains`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sendora
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -113,16 +113,16 @@ for result in batch.results:
|
|
|
113
113
|
|
|
114
114
|
## Messages
|
|
115
115
|
|
|
116
|
-
| Call | Answers
|
|
117
|
-
| --------------------------------------------------------------- |
|
|
118
|
-
| `sendora.messages.get(message_id)` | The message with its recipients, its attachments described and its events
|
|
119
|
-
| `sendora.messages.search(**filters)` | `MessagePage` with `messages` and `next`, newest first
|
|
120
|
-
| `sendora.messages.search_all(**filters)` | Every match, page by page, as an iterator
|
|
121
|
-
| `sendora.inbound.get(inbound_message_id)` | A received message: envelope, parties, headers, text, HTML, attachments described
|
|
122
|
-
| `sendora.inbound.search(**filters)` | `InboundPage` of received mail, newest first
|
|
123
|
-
| `sendora.inbound.search_all(**filters)` | Every received match, page by page, as an iterator
|
|
124
|
-
| `sendora.inbound.raw(inbound_message_id)` | The raw message as `bytes`, `message/rfc822`
|
|
125
|
-
| `sendora.inbound.attachment(inbound_message_id, attachment_id)` | An attachment's `bytes`
|
|
116
|
+
| Call | Answers |
|
|
117
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
118
|
+
| `sendora.messages.get(message_id)` | The message with its recipients, `tracking`, its attachments described and its events |
|
|
119
|
+
| `sendora.messages.search(**filters)` | `MessagePage` with `messages` and `next`, newest first |
|
|
120
|
+
| `sendora.messages.search_all(**filters)` | Every match, page by page, as an iterator |
|
|
121
|
+
| `sendora.inbound.get(inbound_message_id)` | A received message: envelope, parties, headers, text, HTML, attachments described |
|
|
122
|
+
| `sendora.inbound.search(**filters)` | `InboundPage` of received mail, newest first |
|
|
123
|
+
| `sendora.inbound.search_all(**filters)` | Every received match, page by page, as an iterator |
|
|
124
|
+
| `sendora.inbound.raw(inbound_message_id)` | The raw message as `bytes`, `message/rfc822` |
|
|
125
|
+
| `sendora.inbound.attachment(inbound_message_id, attachment_id)` | An attachment's `bytes` |
|
|
126
126
|
|
|
127
127
|
A stream can also receive on a domain of yours: every address on it,
|
|
128
128
|
whatever the local part, lands on the stream. Claim it with the stream
|
|
@@ -146,6 +146,11 @@ list of them), `from_` and `to` as an aware `datetime` or ISO 8601,
|
|
|
146
146
|
`limit` (1 to 100, 50 by default) and `after`, the previous page's
|
|
147
147
|
`next`. Bodies are never returned; the log keeps messages for 13 months.
|
|
148
148
|
|
|
149
|
+
Where mail is tracked, each recipient has `tracked`, `opens`, `clicks`,
|
|
150
|
+
`first_open_at` and `first_human_open_at`. `messages.get` adds `tracking`, and
|
|
151
|
+
its timeline holds `Open` and `Click` events with their labels. Tracking is
|
|
152
|
+
not open to accounts yet.
|
|
153
|
+
|
|
149
154
|
```python
|
|
150
155
|
from datetime import UTC, datetime
|
|
151
156
|
|
|
@@ -198,12 +203,12 @@ message's `substitutions`, up to 20 strings, replace `{{ key }}` in the
|
|
|
198
203
|
subject, the text and the HTML, escaped in the HTML; a key the content
|
|
199
204
|
names that a message lacks raises `substitution_missing`.
|
|
200
205
|
|
|
201
|
-
| Call | Answers
|
|
202
|
-
| ----------------------------------------- |
|
|
203
|
-
| `sendora.broadcasts.send(**broadcast)` | `AcceptedBroadcast` with `broadcast_id`, `total`, `suppressed` and `test`
|
|
204
|
-
| `sendora.broadcasts.get(broadcast_id)` | The broadcast with `status`, `released` and `
|
|
205
|
-
| `sendora.broadcasts.list(limit=, after=)` | `BroadcastPage` with `broadcasts` and `next`, newest first
|
|
206
|
-
| `sendora.broadcasts.cancel(broadcast_id)` | The cancelled broadcast; what had not left counts as failed
|
|
206
|
+
| Call | Answers |
|
|
207
|
+
| ----------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
208
|
+
| `sendora.broadcasts.send(**broadcast)` | `AcceptedBroadcast` with `broadcast_id`, `total`, `suppressed` and `test` |
|
|
209
|
+
| `sendora.broadcasts.get(broadcast_id)` | The broadcast with `status`, `released`, `failed`, `opened`, `clicked` and `links` |
|
|
210
|
+
| `sendora.broadcasts.list(limit=, after=)` | `BroadcastPage` with `broadcasts` and `next`, newest first |
|
|
211
|
+
| `sendora.broadcasts.cancel(broadcast_id)` | The cancelled broadcast; what had not left counts as failed |
|
|
207
212
|
|
|
208
213
|
```python
|
|
209
214
|
accepted = sendora.broadcasts.send(
|
|
@@ -91,16 +91,16 @@ for result in batch.results:
|
|
|
91
91
|
|
|
92
92
|
## Messages
|
|
93
93
|
|
|
94
|
-
| Call | Answers
|
|
95
|
-
| --------------------------------------------------------------- |
|
|
96
|
-
| `sendora.messages.get(message_id)` | The message with its recipients, its attachments described and its events
|
|
97
|
-
| `sendora.messages.search(**filters)` | `MessagePage` with `messages` and `next`, newest first
|
|
98
|
-
| `sendora.messages.search_all(**filters)` | Every match, page by page, as an iterator
|
|
99
|
-
| `sendora.inbound.get(inbound_message_id)` | A received message: envelope, parties, headers, text, HTML, attachments described
|
|
100
|
-
| `sendora.inbound.search(**filters)` | `InboundPage` of received mail, newest first
|
|
101
|
-
| `sendora.inbound.search_all(**filters)` | Every received match, page by page, as an iterator
|
|
102
|
-
| `sendora.inbound.raw(inbound_message_id)` | The raw message as `bytes`, `message/rfc822`
|
|
103
|
-
| `sendora.inbound.attachment(inbound_message_id, attachment_id)` | An attachment's `bytes`
|
|
94
|
+
| Call | Answers |
|
|
95
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
96
|
+
| `sendora.messages.get(message_id)` | The message with its recipients, `tracking`, its attachments described and its events |
|
|
97
|
+
| `sendora.messages.search(**filters)` | `MessagePage` with `messages` and `next`, newest first |
|
|
98
|
+
| `sendora.messages.search_all(**filters)` | Every match, page by page, as an iterator |
|
|
99
|
+
| `sendora.inbound.get(inbound_message_id)` | A received message: envelope, parties, headers, text, HTML, attachments described |
|
|
100
|
+
| `sendora.inbound.search(**filters)` | `InboundPage` of received mail, newest first |
|
|
101
|
+
| `sendora.inbound.search_all(**filters)` | Every received match, page by page, as an iterator |
|
|
102
|
+
| `sendora.inbound.raw(inbound_message_id)` | The raw message as `bytes`, `message/rfc822` |
|
|
103
|
+
| `sendora.inbound.attachment(inbound_message_id, attachment_id)` | An attachment's `bytes` |
|
|
104
104
|
|
|
105
105
|
A stream can also receive on a domain of yours: every address on it,
|
|
106
106
|
whatever the local part, lands on the stream. Claim it with the stream
|
|
@@ -124,6 +124,11 @@ list of them), `from_` and `to` as an aware `datetime` or ISO 8601,
|
|
|
124
124
|
`limit` (1 to 100, 50 by default) and `after`, the previous page's
|
|
125
125
|
`next`. Bodies are never returned; the log keeps messages for 13 months.
|
|
126
126
|
|
|
127
|
+
Where mail is tracked, each recipient has `tracked`, `opens`, `clicks`,
|
|
128
|
+
`first_open_at` and `first_human_open_at`. `messages.get` adds `tracking`, and
|
|
129
|
+
its timeline holds `Open` and `Click` events with their labels. Tracking is
|
|
130
|
+
not open to accounts yet.
|
|
131
|
+
|
|
127
132
|
```python
|
|
128
133
|
from datetime import UTC, datetime
|
|
129
134
|
|
|
@@ -176,12 +181,12 @@ message's `substitutions`, up to 20 strings, replace `{{ key }}` in the
|
|
|
176
181
|
subject, the text and the HTML, escaped in the HTML; a key the content
|
|
177
182
|
names that a message lacks raises `substitution_missing`.
|
|
178
183
|
|
|
179
|
-
| Call | Answers
|
|
180
|
-
| ----------------------------------------- |
|
|
181
|
-
| `sendora.broadcasts.send(**broadcast)` | `AcceptedBroadcast` with `broadcast_id`, `total`, `suppressed` and `test`
|
|
182
|
-
| `sendora.broadcasts.get(broadcast_id)` | The broadcast with `status`, `released` and `
|
|
183
|
-
| `sendora.broadcasts.list(limit=, after=)` | `BroadcastPage` with `broadcasts` and `next`, newest first
|
|
184
|
-
| `sendora.broadcasts.cancel(broadcast_id)` | The cancelled broadcast; what had not left counts as failed
|
|
184
|
+
| Call | Answers |
|
|
185
|
+
| ----------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
186
|
+
| `sendora.broadcasts.send(**broadcast)` | `AcceptedBroadcast` with `broadcast_id`, `total`, `suppressed` and `test` |
|
|
187
|
+
| `sendora.broadcasts.get(broadcast_id)` | The broadcast with `status`, `released`, `failed`, `opened`, `clicked` and `links` |
|
|
188
|
+
| `sendora.broadcasts.list(limit=, after=)` | `BroadcastPage` with `broadcasts` and `next`, newest first |
|
|
189
|
+
| `sendora.broadcasts.cancel(broadcast_id)` | The cancelled broadcast; what had not left counts as failed |
|
|
185
190
|
|
|
186
191
|
```python
|
|
187
192
|
accepted = sendora.broadcasts.send(
|
|
@@ -7,7 +7,13 @@ from urllib.parse import quote
|
|
|
7
7
|
from sendora._answers import decode_sent
|
|
8
8
|
from sendora._transport import AsyncTransport, Call, SyncTransport
|
|
9
9
|
from sendora._wire import decode, encode
|
|
10
|
-
from sendora.types import
|
|
10
|
+
from sendora.types import (
|
|
11
|
+
AcceptedBroadcast,
|
|
12
|
+
Broadcast,
|
|
13
|
+
BroadcastDetail,
|
|
14
|
+
BroadcastPage,
|
|
15
|
+
BroadcastParams,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
class Broadcasts:
|
|
@@ -74,7 +80,7 @@ class Broadcasts:
|
|
|
74
80
|
"""
|
|
75
81
|
return decode(BroadcastPage, self._transport.send(_list_call(limit, after)))
|
|
76
82
|
|
|
77
|
-
def get(self, broadcast_id: str) ->
|
|
83
|
+
def get(self, broadcast_id: str) -> BroadcastDetail:
|
|
78
84
|
"""A broadcast and how far it has come.
|
|
79
85
|
|
|
80
86
|
``released`` and ``failed`` move as the worker hands its messages
|
|
@@ -84,7 +90,7 @@ class Broadcasts:
|
|
|
84
90
|
broadcast_id: The broadcast's id.
|
|
85
91
|
|
|
86
92
|
Returns:
|
|
87
|
-
The broadcast.
|
|
93
|
+
The broadcast, with what tracking recorded of it.
|
|
88
94
|
|
|
89
95
|
Raises:
|
|
90
96
|
NotFoundError: No broadcast of this server has the id.
|
|
@@ -93,7 +99,7 @@ class Broadcasts:
|
|
|
93
99
|
>>> broadcast = sendora.broadcasts.get(broadcast_id)
|
|
94
100
|
>>> print(broadcast.released, "of", broadcast.total)
|
|
95
101
|
"""
|
|
96
|
-
return decode(
|
|
102
|
+
return decode(BroadcastDetail, self._transport.send(_get_call(broadcast_id)))
|
|
97
103
|
|
|
98
104
|
def cancel(self, broadcast_id: str) -> Broadcast:
|
|
99
105
|
"""Holds every message not yet handed to the mail server.
|
|
@@ -186,7 +192,7 @@ class AsyncBroadcasts:
|
|
|
186
192
|
BroadcastPage, await self._transport.send(_list_call(limit, after))
|
|
187
193
|
)
|
|
188
194
|
|
|
189
|
-
async def get(self, broadcast_id: str) ->
|
|
195
|
+
async def get(self, broadcast_id: str) -> BroadcastDetail:
|
|
190
196
|
"""A broadcast and how far it has come.
|
|
191
197
|
|
|
192
198
|
``released`` and ``failed`` move as the worker hands its messages
|
|
@@ -196,7 +202,7 @@ class AsyncBroadcasts:
|
|
|
196
202
|
broadcast_id: The broadcast's id.
|
|
197
203
|
|
|
198
204
|
Returns:
|
|
199
|
-
The broadcast.
|
|
205
|
+
The broadcast, with what tracking recorded of it.
|
|
200
206
|
|
|
201
207
|
Raises:
|
|
202
208
|
NotFoundError: No broadcast of this server has the id.
|
|
@@ -205,7 +211,9 @@ class AsyncBroadcasts:
|
|
|
205
211
|
>>> broadcast = await sendora.broadcasts.get(broadcast_id)
|
|
206
212
|
>>> print(broadcast.released, "of", broadcast.total)
|
|
207
213
|
"""
|
|
208
|
-
return decode(
|
|
214
|
+
return decode(
|
|
215
|
+
BroadcastDetail, await self._transport.send(_get_call(broadcast_id))
|
|
216
|
+
)
|
|
209
217
|
|
|
210
218
|
async def cancel(self, broadcast_id: str) -> Broadcast:
|
|
211
219
|
"""Holds every message not yet handed to the mail server.
|
|
@@ -21,7 +21,7 @@ class Messages:
|
|
|
21
21
|
"""One message as the log holds it, with its timeline.
|
|
22
22
|
|
|
23
23
|
Its recipients with where each stands, its attachments described,
|
|
24
|
-
and
|
|
24
|
+
and the events, oldest first. Its bodies are never returned.
|
|
25
25
|
|
|
26
26
|
Args:
|
|
27
27
|
message_id: The message's id.
|
|
@@ -91,7 +91,7 @@ class AsyncMessages:
|
|
|
91
91
|
"""One message as the log holds it, with its timeline.
|
|
92
92
|
|
|
93
93
|
Its recipients with where each stands, its attachments described,
|
|
94
|
-
and
|
|
94
|
+
and the events, oldest first. Its bodies are never returned.
|
|
95
95
|
|
|
96
96
|
Args:
|
|
97
97
|
message_id: The message's id.
|
|
@@ -39,6 +39,8 @@ __all__ = [
|
|
|
39
39
|
"BouncedDetails",
|
|
40
40
|
"BouncedEvent",
|
|
41
41
|
"Broadcast",
|
|
42
|
+
"BroadcastDetail",
|
|
43
|
+
"BroadcastLink",
|
|
42
44
|
"BroadcastMessageParams",
|
|
43
45
|
"BroadcastPage",
|
|
44
46
|
"BroadcastParams",
|
|
@@ -89,11 +91,13 @@ __all__ = [
|
|
|
89
91
|
"MessageDetail",
|
|
90
92
|
"MessageEvent",
|
|
91
93
|
"MessageEventFields",
|
|
94
|
+
"MessageEventTracking",
|
|
92
95
|
"MessageEventType",
|
|
93
96
|
"MessagePage",
|
|
94
97
|
"MessageRecipient",
|
|
95
98
|
"MessageSearchParams",
|
|
96
99
|
"MessageStatus",
|
|
100
|
+
"MessageTracking",
|
|
97
101
|
"MxRecord",
|
|
98
102
|
"OwnServer",
|
|
99
103
|
"RecipientKind",
|
|
@@ -122,14 +126,21 @@ __all__ = [
|
|
|
122
126
|
"SuppressionReason",
|
|
123
127
|
"Token",
|
|
124
128
|
"TokenList",
|
|
129
|
+
"TrackedLink",
|
|
130
|
+
"TrackingAgent",
|
|
131
|
+
"TrackingAutomated",
|
|
125
132
|
"TrackingCaaCheck",
|
|
126
133
|
"TrackingCaaCheckResult",
|
|
127
134
|
"TrackingCaaRecord",
|
|
135
|
+
"TrackingClient",
|
|
128
136
|
"TrackingCnameCheck",
|
|
129
137
|
"TrackingCnameRecord",
|
|
138
|
+
"TrackingDevice",
|
|
130
139
|
"TrackingDomain",
|
|
131
140
|
"TrackingDomainList",
|
|
132
141
|
"TrackingDomainState",
|
|
142
|
+
"TrackingReason",
|
|
143
|
+
"TrackingSkipped",
|
|
133
144
|
"TxtRecord",
|
|
134
145
|
"UnknownEvent",
|
|
135
146
|
"UnsubscribedDetails",
|
|
@@ -472,10 +483,58 @@ MessageEventType = Literal[
|
|
|
472
483
|
"DeferredInjectionRebind",
|
|
473
484
|
"Rejection",
|
|
474
485
|
"Delayed",
|
|
486
|
+
"Open",
|
|
487
|
+
"Click",
|
|
475
488
|
]
|
|
476
489
|
"""A step of a message's timeline: ``Reception`` when the mail server took
|
|
477
490
|
it, ``Delivery``, ``TransientFailure``, ``Bounce``, ``OOB`` for a bounce
|
|
478
|
-
report received by mail, ``Feedback`` for a spam complaint
|
|
491
|
+
report received by mail, ``Feedback`` for a spam complaint, ``Open`` and
|
|
492
|
+
``Click`` for what tracking recorded."""
|
|
493
|
+
|
|
494
|
+
TrackingAutomated = Literal["likely", "unlikely", "unknown"]
|
|
495
|
+
"""Whether a machine rather than a person did it: ``unknown`` where nobody
|
|
496
|
+
can tell, as with Apple Mail."""
|
|
497
|
+
|
|
498
|
+
TrackingAgent = Literal[
|
|
499
|
+
"apple_privacy",
|
|
500
|
+
"gmail_prefetch",
|
|
501
|
+
"gmail_proxy",
|
|
502
|
+
"yahoo_proxy",
|
|
503
|
+
"outlook_prefetch",
|
|
504
|
+
"proton_proxy",
|
|
505
|
+
"scanner",
|
|
506
|
+
"crawler",
|
|
507
|
+
]
|
|
508
|
+
"""The proxy or machine an open or a click came through."""
|
|
509
|
+
|
|
510
|
+
TrackingReason = Literal[
|
|
511
|
+
"privacy_proxy",
|
|
512
|
+
"prefetch_signature",
|
|
513
|
+
"delivery_timing",
|
|
514
|
+
"link_burst",
|
|
515
|
+
"scanner_network",
|
|
516
|
+
"bot_user_agent",
|
|
517
|
+
"many_networks",
|
|
518
|
+
]
|
|
519
|
+
"""Why an open or a click was labelled a machine's."""
|
|
520
|
+
|
|
521
|
+
TrackingClient = Literal[
|
|
522
|
+
"apple_mail",
|
|
523
|
+
"gmail",
|
|
524
|
+
"outlook",
|
|
525
|
+
"yahoo",
|
|
526
|
+
"thunderbird",
|
|
527
|
+
"proton",
|
|
528
|
+
"browser",
|
|
529
|
+
"other",
|
|
530
|
+
]
|
|
531
|
+
"""The mail program an open or a click came from."""
|
|
532
|
+
|
|
533
|
+
TrackingDevice = Literal["desktop", "mobile", "tablet"]
|
|
534
|
+
"""The kind of device an open or a click came from."""
|
|
535
|
+
|
|
536
|
+
TrackingSkipped = Literal["no_tracking_domain", "gmail_clipping"]
|
|
537
|
+
"""Why tracking stayed out of at least one copy of a message."""
|
|
479
538
|
|
|
480
539
|
|
|
481
540
|
@model
|
|
@@ -486,11 +545,25 @@ class MessageRecipient(Answer):
|
|
|
486
545
|
address: The address.
|
|
487
546
|
kind: Whether it was in ``to``, ``cc`` or ``bcc``.
|
|
488
547
|
status: Where its delivery stands.
|
|
548
|
+
tracked: Whether tracking applied to its copy when it left. It does
|
|
549
|
+
not say to opens, links or both, or that the HTML held a link to
|
|
550
|
+
track.
|
|
551
|
+
opens: Opens recorded, machines' included.
|
|
552
|
+
clicks: Clicks recorded, machines' included.
|
|
553
|
+
first_open_at: The first open, or the first click that was likely a
|
|
554
|
+
person's; ``None`` before that.
|
|
555
|
+
first_human_open_at: The same, counting only what was likely a
|
|
556
|
+
person; ``None`` before that.
|
|
489
557
|
"""
|
|
490
558
|
|
|
491
559
|
address: str
|
|
492
560
|
kind: RecipientKind
|
|
493
561
|
status: RecipientStatus
|
|
562
|
+
tracked: bool
|
|
563
|
+
opens: int
|
|
564
|
+
clicks: int
|
|
565
|
+
first_open_at: datetime | None
|
|
566
|
+
first_human_open_at: datetime | None
|
|
494
567
|
|
|
495
568
|
|
|
496
569
|
@model
|
|
@@ -542,6 +615,47 @@ class MessageAttachment(Answer):
|
|
|
542
615
|
size: int
|
|
543
616
|
|
|
544
617
|
|
|
618
|
+
@model
|
|
619
|
+
class TrackedLink(Answer):
|
|
620
|
+
"""The link a recipient clicked.
|
|
621
|
+
|
|
622
|
+
Attributes:
|
|
623
|
+
index: Its position in the HTML, the same in every copy, counting
|
|
624
|
+
from 0.
|
|
625
|
+
url: Where it led; ``None`` once it has gone with the message's text
|
|
626
|
+
and HTML.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
index: int
|
|
630
|
+
url: str | None
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@model
|
|
634
|
+
class MessageEventTracking(Answer):
|
|
635
|
+
"""What tracking knows of an ``Open`` or a ``Click``.
|
|
636
|
+
|
|
637
|
+
Attributes:
|
|
638
|
+
automated: Whether a machine rather than a person did it.
|
|
639
|
+
agent: The proxy or machine it came through, where Sendora knows it.
|
|
640
|
+
reasons: Why it was labelled a machine's.
|
|
641
|
+
client: The mail program, where Sendora knows it.
|
|
642
|
+
device: The kind of device, where Sendora knows it.
|
|
643
|
+
first: The recipient's first event of this kind.
|
|
644
|
+
first_human: The recipient's first event of this kind that was
|
|
645
|
+
likely a person's.
|
|
646
|
+
link: The link clicked; ``None`` on an ``Open``.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
automated: TrackingAutomated
|
|
650
|
+
agent: TrackingAgent | None
|
|
651
|
+
reasons: list[TrackingReason]
|
|
652
|
+
client: TrackingClient | None
|
|
653
|
+
device: TrackingDevice | None
|
|
654
|
+
first: bool
|
|
655
|
+
first_human: bool
|
|
656
|
+
link: TrackedLink | None
|
|
657
|
+
|
|
658
|
+
|
|
545
659
|
@model
|
|
546
660
|
class MessageEvent(Answer):
|
|
547
661
|
"""One step of a message's timeline.
|
|
@@ -555,6 +669,8 @@ class MessageEvent(Answer):
|
|
|
555
669
|
details: The receiver's response text.
|
|
556
670
|
classification: How a bounce was classified, such as
|
|
557
671
|
``InvalidRecipient``.
|
|
672
|
+
tracking: What tracking knows of an ``Open`` or a ``Click``; ``None``
|
|
673
|
+
on any other event.
|
|
558
674
|
"""
|
|
559
675
|
|
|
560
676
|
recipient: str | None
|
|
@@ -563,6 +679,28 @@ class MessageEvent(Answer):
|
|
|
563
679
|
code: int | None
|
|
564
680
|
details: str | None
|
|
565
681
|
classification: str | None
|
|
682
|
+
tracking: MessageEventTracking | None
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
@model
|
|
686
|
+
class MessageTracking(Answer):
|
|
687
|
+
"""Whether opens and clicks are recorded now, and why tracking stayed out.
|
|
688
|
+
|
|
689
|
+
Attributes:
|
|
690
|
+
recording_opens: Whether new opens of this message are recorded now,
|
|
691
|
+
by its own setting, then its broadcast's, then its stream's at
|
|
692
|
+
this moment. This says nothing of what a copy already sent
|
|
693
|
+
carries.
|
|
694
|
+
recording_clicks: The same for clicks on its links.
|
|
695
|
+
skipped: Why tracking stayed out of at least one copy: the from
|
|
696
|
+
domain had no working tracking domain, or tracked links would
|
|
697
|
+
have taken the HTML past where Gmail clips it. Each recipient's
|
|
698
|
+
``tracked`` says whether its copy was tracked.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
recording_opens: bool
|
|
702
|
+
recording_clicks: bool
|
|
703
|
+
skipped: TrackingSkipped | None
|
|
566
704
|
|
|
567
705
|
|
|
568
706
|
@model
|
|
@@ -570,10 +708,13 @@ class MessageDetail(Message):
|
|
|
570
708
|
"""A message with its attachments described and its timeline.
|
|
571
709
|
|
|
572
710
|
Attributes:
|
|
711
|
+
tracking: Whether opens and clicks are recorded now.
|
|
573
712
|
attachments: Each attachment, described.
|
|
574
|
-
events:
|
|
713
|
+
events: The events, oldest first. Opens and clicks are capped, while
|
|
714
|
+
each recipient's ``opens`` and ``clicks`` count them all.
|
|
575
715
|
"""
|
|
576
716
|
|
|
717
|
+
tracking: MessageTracking
|
|
577
718
|
attachments: list[MessageAttachment]
|
|
578
719
|
events: list[MessageEvent]
|
|
579
720
|
|
|
@@ -751,6 +892,47 @@ class Broadcast(Answer):
|
|
|
751
892
|
completed_at: datetime | None
|
|
752
893
|
|
|
753
894
|
|
|
895
|
+
@model
|
|
896
|
+
class BroadcastLink(Answer):
|
|
897
|
+
"""A tracked link of a broadcast that has a click.
|
|
898
|
+
|
|
899
|
+
Attributes:
|
|
900
|
+
index: Its position in the HTML, the same in every copy, counting
|
|
901
|
+
from 0.
|
|
902
|
+
url: Where the first click led, since a substitution can give a link
|
|
903
|
+
another URL per recipient; ``None`` once it has gone with the
|
|
904
|
+
content.
|
|
905
|
+
clicked: Recipients who clicked this link, machines' included.
|
|
906
|
+
clicked_by_people: Those of them likely to be people.
|
|
907
|
+
"""
|
|
908
|
+
|
|
909
|
+
index: int
|
|
910
|
+
url: str | None
|
|
911
|
+
clicked: int
|
|
912
|
+
clicked_by_people: int
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
@model
|
|
916
|
+
class BroadcastDetail(Broadcast):
|
|
917
|
+
"""A broadcast, how far it has come and what tracking recorded of it.
|
|
918
|
+
|
|
919
|
+
Attributes:
|
|
920
|
+
opened: Recipients with an open, machines' included, or a click that
|
|
921
|
+
was likely a person's.
|
|
922
|
+
opened_by_people: Those of them likely to be people.
|
|
923
|
+
clicked: Recipients who clicked a link, machines' included.
|
|
924
|
+
clicked_by_people: Those of them likely to be people.
|
|
925
|
+
links: Each tracked link that has a click, by its index, counted from
|
|
926
|
+
the clicks each recipient keeps.
|
|
927
|
+
"""
|
|
928
|
+
|
|
929
|
+
opened: int
|
|
930
|
+
opened_by_people: int
|
|
931
|
+
clicked: int
|
|
932
|
+
clicked_by_people: int
|
|
933
|
+
links: list[BroadcastLink]
|
|
934
|
+
|
|
935
|
+
|
|
754
936
|
@model
|
|
755
937
|
class BroadcastPage(Answer):
|
|
756
938
|
"""One page of the server's broadcasts, newest first.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|