stackshift 0.1.2__tar.gz → 1.0.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.
- {stackshift-0.1.2 → stackshift-1.0.0}/PKG-INFO +1 -1
- {stackshift-0.1.2 → stackshift-1.0.0}/README.md +2 -2
- {stackshift-0.1.2 → stackshift-1.0.0}/pyproject.toml +1 -1
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/__init__.py +8 -1
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/client.py +1 -1
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail.py +35 -3
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail_advanced.py +21 -1
- stackshift-1.0.0/stackshift/mail_campaigns.py +61 -0
- stackshift-0.1.2/stackshift/mail_phase7.py → stackshift-1.0.0/stackshift/mail_events_webhooks.py +8 -0
- stackshift-1.0.0/stackshift/mail_exports.py +30 -0
- stackshift-1.0.0/stackshift/mail_streams.py +18 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/PKG-INFO +1 -1
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/SOURCES.txt +7 -2
- {stackshift-0.1.2 → stackshift-1.0.0}/tests/test_mail.py +7 -7
- {stackshift-0.1.2 → stackshift-1.0.0}/tests/test_mail_advanced.py +11 -2
- stackshift-1.0.0/tests/test_mail_diagnostics.py +22 -0
- stackshift-0.1.2/tests/test_mail_phase7.py → stackshift-1.0.0/tests/test_mail_events_webhooks.py +8 -8
- stackshift-1.0.0/tests/test_mail_exports.py +30 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/setup.cfg +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/assets.py +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail_reputation.py +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/projects.py +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/dependency_links.txt +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/requires.txt +0 -0
- {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/top_level.txt +0 -0
|
@@ -29,13 +29,13 @@ print(message["id"], message["status"], message["idempotencyStatus"])
|
|
|
29
29
|
Inspect message status:
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
|
-
messages = stackshift.mail.messages.list(status="
|
|
32
|
+
messages = stackshift.mail.messages.list(status="mta_accepted", limit=20)
|
|
33
33
|
detail = stackshift.mail.messages.get(messages["data"][0]["id"])
|
|
34
34
|
attempts = stackshift.mail.messages.attempts(detail["id"])
|
|
35
35
|
logs = stackshift.mail.messages.logs(detail["id"])
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
`mta_accepted` means the message was accepted by StackShift's outbound MTA. It does not mean recipient-MX acceptance, inbox placement, opens, clicks, or spam placement. Recipient records later transition independently to `delayed`, `delivered`, `bounced`, `failed`, `suppressed`, or `complained`.
|
|
39
39
|
|
|
40
40
|
## Events and webhooks
|
|
41
41
|
|
|
@@ -8,7 +8,10 @@ from .mail import (
|
|
|
8
8
|
MailTemplatesClient,
|
|
9
9
|
)
|
|
10
10
|
from .mail_reputation import MailLimitsClient, MailReputationClient, MailReputationEventsClient
|
|
11
|
-
from .
|
|
11
|
+
from .mail_events_webhooks import MailEventsClient, MailWebhooksClient, verify_webhook_signature
|
|
12
|
+
from .mail_streams import MailStreamsClient
|
|
13
|
+
from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
|
|
14
|
+
from .mail_exports import MailExportsClient
|
|
12
15
|
from .projects import ProjectsClient
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
@@ -25,6 +28,10 @@ __all__ = [
|
|
|
25
28
|
"MailReputationEventsClient",
|
|
26
29
|
"MailTemplatesClient",
|
|
27
30
|
"MailWebhooksClient",
|
|
31
|
+
"MailStreamsClient",
|
|
32
|
+
"MailAudiencesClient",
|
|
33
|
+
"MailCampaignsClient",
|
|
34
|
+
"MailExportsClient",
|
|
28
35
|
"ProjectsClient",
|
|
29
36
|
"asset_transform_options",
|
|
30
37
|
"asset_transform_spec",
|
|
@@ -43,7 +43,7 @@ class StackShift:
|
|
|
43
43
|
def _request(self, base_url: str, method: str, path: str, **kwargs: Any) -> Any:
|
|
44
44
|
headers = kwargs.pop("headers", {})
|
|
45
45
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
46
|
-
headers["User-Agent"] = "StackShift-Python/0
|
|
46
|
+
headers["User-Agent"] = "StackShift-Python/1.0"
|
|
47
47
|
|
|
48
48
|
response = self.session.request(method, f"{base_url}{path}", headers=headers, **kwargs)
|
|
49
49
|
payload = response.json() if response.content else None
|
|
@@ -4,8 +4,11 @@ from typing import Any
|
|
|
4
4
|
from urllib.parse import quote
|
|
5
5
|
|
|
6
6
|
from .mail_advanced import MailAnalyticsClient, MailBatchClient, MailInboundClient, MailScheduledClient
|
|
7
|
-
from .
|
|
7
|
+
from .mail_events_webhooks import MailEventsClient, MailWebhooksClient
|
|
8
8
|
from .mail_reputation import MailLimitsClient, MailReputationClient
|
|
9
|
+
from .mail_streams import MailStreamsClient
|
|
10
|
+
from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
|
|
11
|
+
from .mail_exports import MailExportsClient
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class MailClient:
|
|
@@ -19,6 +22,10 @@ class MailClient:
|
|
|
19
22
|
self.templates = MailTemplatesClient(client)
|
|
20
23
|
self.events = MailEventsClient(client)
|
|
21
24
|
self.webhooks = MailWebhooksClient(client)
|
|
25
|
+
self.streams = MailStreamsClient(client)
|
|
26
|
+
self.audiences = MailAudiencesClient(client)
|
|
27
|
+
self.campaigns = MailCampaignsClient(client)
|
|
28
|
+
self.exports = MailExportsClient(client)
|
|
22
29
|
self.limits = MailLimitsClient(client)
|
|
23
30
|
self.reputation = MailReputationClient(client)
|
|
24
31
|
self.scheduled = MailScheduledClient(client)
|
|
@@ -39,6 +46,9 @@ class MailClient:
|
|
|
39
46
|
reply_to: str | dict[str, str] | None = None,
|
|
40
47
|
idempotency_key: str | None = None,
|
|
41
48
|
attachments: list[dict[str, Any]] | None = None,
|
|
49
|
+
headers: dict[str, str] | None = None,
|
|
50
|
+
tags: dict[str, str] | None = None,
|
|
51
|
+
stream: str | None = None,
|
|
42
52
|
) -> dict[str, Any]:
|
|
43
53
|
payload = {
|
|
44
54
|
"from": from_,
|
|
@@ -51,6 +61,9 @@ class MailClient:
|
|
|
51
61
|
"text": text,
|
|
52
62
|
"idempotencyKey": idempotency_key,
|
|
53
63
|
"attachments": attachments,
|
|
64
|
+
"headers": headers,
|
|
65
|
+
"tags": tags,
|
|
66
|
+
"stream": stream,
|
|
54
67
|
}
|
|
55
68
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
56
69
|
return self._client.mail_request("POST", "/mail/send", json=clean)
|
|
@@ -68,6 +81,9 @@ class MailClient:
|
|
|
68
81
|
reply_to: str | dict[str, str] | None = None,
|
|
69
82
|
idempotency_key: str | None = None,
|
|
70
83
|
attachments: list[dict[str, Any]] | None = None,
|
|
84
|
+
headers: dict[str, str] | None = None,
|
|
85
|
+
tags: dict[str, str] | None = None,
|
|
86
|
+
stream: str | None = None,
|
|
71
87
|
) -> dict[str, Any]:
|
|
72
88
|
payload = {
|
|
73
89
|
"template": template,
|
|
@@ -80,6 +96,9 @@ class MailClient:
|
|
|
80
96
|
"data": data,
|
|
81
97
|
"idempotencyKey": idempotency_key,
|
|
82
98
|
"attachments": attachments,
|
|
99
|
+
"headers": headers,
|
|
100
|
+
"tags": tags,
|
|
101
|
+
"stream": stream,
|
|
83
102
|
}
|
|
84
103
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
85
104
|
return self._client.mail_request("POST", "/mail/send-template", json=clean)
|
|
@@ -162,6 +181,18 @@ class MailTemplatesClient:
|
|
|
162
181
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
163
182
|
return self._client.mail_request("POST", f"/mail/templates/{quote(template_id)}/preview", json=clean)
|
|
164
183
|
|
|
184
|
+
def diagnostics(
|
|
185
|
+
self,
|
|
186
|
+
template_id: str,
|
|
187
|
+
*,
|
|
188
|
+
data: dict[str, Any] | None = None,
|
|
189
|
+
version_id: str | None = None,
|
|
190
|
+
stream: str | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
payload = {"data": data, "versionId": version_id, "stream": stream}
|
|
193
|
+
clean = {key: value for key, value in payload.items() if value is not None}
|
|
194
|
+
return self._client.mail_request("POST", f"/mail/templates/{quote(template_id)}/diagnostics", json=clean)
|
|
195
|
+
|
|
165
196
|
def test_send(
|
|
166
197
|
self,
|
|
167
198
|
template_id: str,
|
|
@@ -231,8 +262,9 @@ class MailMessagesClient:
|
|
|
231
262
|
def bounces(self, message_id: str) -> dict[str, Any]:
|
|
232
263
|
return self._client.mail_request("GET", f"/mail/messages/{quote(message_id)}/bounces")
|
|
233
264
|
|
|
234
|
-
def timeline(self, message_id: str) -> dict[str, Any]:
|
|
235
|
-
|
|
265
|
+
def timeline(self, message_id: str, *, recipient: str | None = None) -> dict[str, Any]:
|
|
266
|
+
params = {"recipient": recipient} if recipient else None
|
|
267
|
+
return self._client.mail_request("GET", f"/mail/messages/{quote(message_id)}/timeline", params=params)
|
|
236
268
|
|
|
237
269
|
|
|
238
270
|
class MailSuppressionsClient:
|
|
@@ -36,11 +36,31 @@ class MailBatchClient:
|
|
|
36
36
|
from_: str | dict[str, str],
|
|
37
37
|
recipients: list[dict[str, Any]],
|
|
38
38
|
version_id: str | None = None,
|
|
39
|
+
headers: dict[str, str] | None = None,
|
|
40
|
+
tags: dict[str, str] | None = None,
|
|
41
|
+
stream: str | None = None,
|
|
39
42
|
) -> dict[str, Any]:
|
|
40
|
-
payload = {
|
|
43
|
+
payload = {
|
|
44
|
+
"template": template,
|
|
45
|
+
"from": from_,
|
|
46
|
+
"recipients": recipients,
|
|
47
|
+
"versionId": version_id,
|
|
48
|
+
"headers": headers,
|
|
49
|
+
"tags": tags,
|
|
50
|
+
"stream": stream,
|
|
51
|
+
}
|
|
41
52
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
42
53
|
return self._client.mail_request("POST", "/mail/batch-template", json=clean)
|
|
43
54
|
|
|
55
|
+
def list(self) -> dict[str, Any]:
|
|
56
|
+
return self._client.mail_request("GET", "/mail/batches")
|
|
57
|
+
|
|
58
|
+
def get(self, batch_id: str) -> dict[str, Any]:
|
|
59
|
+
return self._client.mail_request("GET", f"/mail/batches/{quote(batch_id)}")
|
|
60
|
+
|
|
61
|
+
def items(self, batch_id: str) -> dict[str, Any]:
|
|
62
|
+
return self._client.mail_request("GET", f"/mail/batches/{quote(batch_id)}/items")
|
|
63
|
+
|
|
44
64
|
|
|
45
65
|
class MailInboundClient:
|
|
46
66
|
def __init__(self, client: Any) -> None:
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MailAudiencesClient:
|
|
8
|
+
def __init__(self, client: Any) -> None:
|
|
9
|
+
self._client = client
|
|
10
|
+
|
|
11
|
+
def create(self, *, name: str, description: str | None = None) -> dict[str, Any]:
|
|
12
|
+
payload = {"name": name, "description": description}
|
|
13
|
+
return self._client.mail_request("POST", "/mail/audiences", json={key: value for key, value in payload.items() if value is not None})
|
|
14
|
+
|
|
15
|
+
def list(self) -> dict[str, Any]:
|
|
16
|
+
return self._client.mail_request("GET", "/mail/audiences")
|
|
17
|
+
|
|
18
|
+
def get(self, audience_id: str) -> dict[str, Any]:
|
|
19
|
+
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id)}")
|
|
20
|
+
|
|
21
|
+
def members(self, audience_id: str) -> dict[str, Any]:
|
|
22
|
+
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id)}/members")
|
|
23
|
+
|
|
24
|
+
def import_members(self, audience_id: str, members: list[dict[str, Any]]) -> dict[str, Any]:
|
|
25
|
+
return self._client.mail_request(
|
|
26
|
+
"POST",
|
|
27
|
+
f"/mail/audiences/{quote(audience_id)}/imports",
|
|
28
|
+
json={"members": members},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MailCampaignsClient:
|
|
33
|
+
def __init__(self, client: Any) -> None:
|
|
34
|
+
self._client = client
|
|
35
|
+
|
|
36
|
+
def create(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
name: str,
|
|
40
|
+
audience_id: str,
|
|
41
|
+
template: str,
|
|
42
|
+
from_: str,
|
|
43
|
+
version_id: str | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
payload = {
|
|
46
|
+
"name": name,
|
|
47
|
+
"audienceId": audience_id,
|
|
48
|
+
"template": template,
|
|
49
|
+
"from": from_,
|
|
50
|
+
"versionId": version_id,
|
|
51
|
+
}
|
|
52
|
+
return self._client.mail_request("POST", "/mail/campaigns", json={key: value for key, value in payload.items() if value is not None})
|
|
53
|
+
|
|
54
|
+
def list(self) -> dict[str, Any]:
|
|
55
|
+
return self._client.mail_request("GET", "/mail/campaigns")
|
|
56
|
+
|
|
57
|
+
def get(self, campaign_id: str) -> dict[str, Any]:
|
|
58
|
+
return self._client.mail_request("GET", f"/mail/campaigns/{quote(campaign_id)}")
|
|
59
|
+
|
|
60
|
+
def send(self, campaign_id: str) -> dict[str, Any]:
|
|
61
|
+
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id)}/send")
|
stackshift-0.1.2/stackshift/mail_phase7.py → stackshift-1.0.0/stackshift/mail_events_webhooks.py
RENAMED
|
@@ -100,6 +100,14 @@ class MailWebhooksClient:
|
|
|
100
100
|
def retry_delivery(self, delivery_id: str) -> dict[str, Any]:
|
|
101
101
|
return self._client.mail_request("POST", f"/mail/webhook-deliveries/{quote(delivery_id)}/retry")
|
|
102
102
|
|
|
103
|
+
def replay(self, webhook_id: str, *, event_ids: list[str], idempotency_key: str) -> dict[str, Any]:
|
|
104
|
+
return self._client.mail_request(
|
|
105
|
+
"POST",
|
|
106
|
+
f"/mail/webhooks/{quote(webhook_id)}/replay",
|
|
107
|
+
headers={"Idempotency-Key": idempotency_key},
|
|
108
|
+
json={"eventIds": event_ids},
|
|
109
|
+
)
|
|
110
|
+
|
|
103
111
|
def verify_signature(
|
|
104
112
|
self,
|
|
105
113
|
*,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MailExportsClient:
|
|
9
|
+
def __init__(self, client: Any) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def create(self, *, from_: datetime | None = None, to: datetime | None = None) -> dict[str, Any]:
|
|
13
|
+
payload = {
|
|
14
|
+
"kind": "recipient_delivery",
|
|
15
|
+
"format": "jsonl",
|
|
16
|
+
"from": from_.isoformat() if from_ else None,
|
|
17
|
+
"to": to.isoformat() if to else None,
|
|
18
|
+
}
|
|
19
|
+
return self._client.mail_request(
|
|
20
|
+
"POST", "/mail/exports", json={key: value for key, value in payload.items() if value is not None}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def list(self) -> dict[str, Any]:
|
|
24
|
+
return self._client.mail_request("GET", "/mail/exports")
|
|
25
|
+
|
|
26
|
+
def get(self, export_id: str) -> dict[str, Any]:
|
|
27
|
+
return self._client.mail_request("GET", f"/mail/exports/{quote(export_id)}")
|
|
28
|
+
|
|
29
|
+
def download_path(self, export_id: str) -> str:
|
|
30
|
+
return f"/mail/exports/{quote(export_id)}/download"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MailStreamsClient:
|
|
7
|
+
def __init__(self, client: Any) -> None:
|
|
8
|
+
self._client = client
|
|
9
|
+
|
|
10
|
+
def create(self, *, name: str, slug: str, type: str) -> dict[str, Any]:
|
|
11
|
+
return self._client.mail_request(
|
|
12
|
+
"POST",
|
|
13
|
+
"/mail/streams",
|
|
14
|
+
json={"name": name, "slug": slug, "type": type},
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def list(self) -> dict[str, Any]:
|
|
18
|
+
return self._client.mail_request("GET", "/mail/streams")
|
|
@@ -5,8 +5,11 @@ stackshift/assets.py
|
|
|
5
5
|
stackshift/client.py
|
|
6
6
|
stackshift/mail.py
|
|
7
7
|
stackshift/mail_advanced.py
|
|
8
|
-
stackshift/
|
|
8
|
+
stackshift/mail_campaigns.py
|
|
9
|
+
stackshift/mail_events_webhooks.py
|
|
10
|
+
stackshift/mail_exports.py
|
|
9
11
|
stackshift/mail_reputation.py
|
|
12
|
+
stackshift/mail_streams.py
|
|
10
13
|
stackshift/projects.py
|
|
11
14
|
stackshift.egg-info/PKG-INFO
|
|
12
15
|
stackshift.egg-info/SOURCES.txt
|
|
@@ -15,4 +18,6 @@ stackshift.egg-info/requires.txt
|
|
|
15
18
|
stackshift.egg-info/top_level.txt
|
|
16
19
|
tests/test_mail.py
|
|
17
20
|
tests/test_mail_advanced.py
|
|
18
|
-
tests/
|
|
21
|
+
tests/test_mail_diagnostics.py
|
|
22
|
+
tests/test_mail_events_webhooks.py
|
|
23
|
+
tests/test_mail_exports.py
|
|
@@ -96,9 +96,9 @@ class FakeSession:
|
|
|
96
96
|
if url.endswith("/mail/bounces"):
|
|
97
97
|
return FakeResponse(200, {"data": [bounce_response()], "nextCursor": None})
|
|
98
98
|
if url.endswith("/mail/messages/msg_123"):
|
|
99
|
-
return FakeResponse(200, {"id": "msg_123", "from": {"email": "noreply@mail.stackshift.cloud"}, "to": ["ada@example.com"], "cc": [], "bcc": [], "subject": "Welcome", "status": "
|
|
99
|
+
return FakeResponse(200, {"id": "msg_123", "from": {"email": "noreply@mail.stackshift.cloud"}, "to": ["ada@example.com"], "cc": [], "bcc": [], "subject": "Welcome", "status": "mta_accepted"})
|
|
100
100
|
if url.endswith("/mail/messages"):
|
|
101
|
-
return FakeResponse(200, {"data": [{"id": "msg_123", "status": "
|
|
101
|
+
return FakeResponse(200, {"data": [{"id": "msg_123", "status": "mta_accepted"}], "nextCursor": None})
|
|
102
102
|
if url.endswith("/assets/transformations") and method == "GET":
|
|
103
103
|
return FakeResponse(200, {"success": True, "data": {"transformations": [{"id": "tr_1", "name": "custom-card", "normalized_spec": "w_640,h_360,c_fill,f_webp,q_82"}]}})
|
|
104
104
|
if url.endswith("/assets/transformations") and method == "POST":
|
|
@@ -152,12 +152,12 @@ class MailSDKTest(unittest.TestCase):
|
|
|
152
152
|
self.assertEqual(session.calls[0][1], "https://api.stackshift.cloud/v1/mail/send")
|
|
153
153
|
self.assertEqual(session.calls[0][2]["headers"]["Authorization"], "Bearer sk_test")
|
|
154
154
|
|
|
155
|
-
messages = stackshift.mail.messages.list(status="
|
|
156
|
-
self.assertEqual(messages["data"][0]["status"], "
|
|
157
|
-
self.assertEqual(session.calls[1][2]["params"]["status"], "
|
|
155
|
+
messages = stackshift.mail.messages.list(status="mta_accepted", limit=20)
|
|
156
|
+
self.assertEqual(messages["data"][0]["status"], "mta_accepted")
|
|
157
|
+
self.assertEqual(session.calls[1][2]["params"]["status"], "mta_accepted")
|
|
158
158
|
|
|
159
159
|
message = stackshift.mail.messages.get("msg_123")
|
|
160
|
-
self.assertEqual(message["status"], "
|
|
160
|
+
self.assertEqual(message["status"], "mta_accepted")
|
|
161
161
|
|
|
162
162
|
attempts = stackshift.mail.messages.attempts("msg_123")
|
|
163
163
|
self.assertEqual(attempts["data"][0]["status"], "accepted_by_mta")
|
|
@@ -312,7 +312,7 @@ def domain_response(status):
|
|
|
312
312
|
{
|
|
313
313
|
"type": "TXT",
|
|
314
314
|
"name": "acme.com",
|
|
315
|
-
"value": "v=spf1 include:_spf.stackshift.
|
|
315
|
+
"value": "v=spf1 include:_spf.stackshift.cloud ~all",
|
|
316
316
|
"status": "verified",
|
|
317
317
|
"required": True,
|
|
318
318
|
}
|
|
@@ -39,6 +39,12 @@ class FakeSession:
|
|
|
39
39
|
return FakeResponse(202, {"id": "batch_tpl_123", "status": "queued", "messageCount": 1})
|
|
40
40
|
if url.endswith("/mail/batch"):
|
|
41
41
|
return FakeResponse(202, {"id": "batch_123", "status": "queued", "messageCount": 1})
|
|
42
|
+
if url.endswith("/mail/batches/batch_123/items"):
|
|
43
|
+
return FakeResponse(200, {"data": [{"id": "item_123", "recipients": [{"email": "ada@example.com", "status": "delivered"}]}]})
|
|
44
|
+
if url.endswith("/mail/batches/batch_123"):
|
|
45
|
+
return FakeResponse(200, {"id": "batch_123", "status": "completed", "messageCount": 1})
|
|
46
|
+
if url.endswith("/mail/batches"):
|
|
47
|
+
return FakeResponse(200, {"data": [{"id": "batch_123", "status": "completed", "messageCount": 1}]})
|
|
42
48
|
if url.endswith("/mail/inbound/domains") and method == "POST":
|
|
43
49
|
return FakeResponse(201, {"id": "ind_123", "domain": "inbound.example.com", "status": "pending"})
|
|
44
50
|
if url.endswith("/mail/inbound/domains"):
|
|
@@ -50,7 +56,7 @@ class FakeSession:
|
|
|
50
56
|
if url.endswith("/mail/inbound/messages"):
|
|
51
57
|
return FakeResponse(200, {"data": [{"id": "inm_123", "fromEmail": "ada@example.com", "toEmails": ["support@example.com"]}]})
|
|
52
58
|
if url.endswith("/mail/analytics"):
|
|
53
|
-
return FakeResponse(200, {"summary": {"
|
|
59
|
+
return FakeResponse(200, {"summary": {"mtaAccepted": 1}, "series": [{"bucket": "2026-05-11", "mtaAccepted": 1}]})
|
|
54
60
|
return FakeResponse(404, {"error": {"code": "not_found", "message": "Not found."}})
|
|
55
61
|
|
|
56
62
|
|
|
@@ -80,6 +86,9 @@ class MailAdvancedSDKTest(unittest.TestCase):
|
|
|
80
86
|
recipients=[{"to": "ada@example.com", "data": {"name": "Ada"}}],
|
|
81
87
|
)
|
|
82
88
|
self.assertEqual(template_batch["id"], "batch_tpl_123")
|
|
89
|
+
self.assertEqual(stackshift.mail.batch.list()["data"][0]["id"], "batch_123")
|
|
90
|
+
self.assertEqual(stackshift.mail.batch.get("batch_123")["status"], "completed")
|
|
91
|
+
self.assertEqual(stackshift.mail.batch.items("batch_123")["data"][0]["recipients"][0]["status"], "delivered")
|
|
83
92
|
|
|
84
93
|
self.assertEqual(stackshift.mail.inbound.domains.create("inbound.example.com")["id"], "ind_123")
|
|
85
94
|
self.assertEqual(stackshift.mail.inbound.domains.list()["data"][0]["status"], "verified")
|
|
@@ -88,7 +97,7 @@ class MailAdvancedSDKTest(unittest.TestCase):
|
|
|
88
97
|
self.assertEqual(stackshift.mail.inbound.messages.get("inm_123")["fromEmail"], "ada@example.com")
|
|
89
98
|
|
|
90
99
|
analytics = stackshift.mail.analytics.get(range_="7d", interval="day", domain="example.com")
|
|
91
|
-
self.assertEqual(analytics["summary"]["
|
|
100
|
+
self.assertEqual(analytics["summary"]["mtaAccepted"], 1)
|
|
92
101
|
self.assertEqual(session.calls[-1][2]["params"]["domain"], "example.com")
|
|
93
102
|
|
|
94
103
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from stackshift.mail import MailTemplatesClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeClient:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.calls = []
|
|
9
|
+
|
|
10
|
+
def mail_request(self, method, path, **kwargs):
|
|
11
|
+
self.calls.append((method, path, kwargs))
|
|
12
|
+
return {"status": "warning", "findings": [{"code": "remote_images"}]}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MailDiagnosticsTest(unittest.TestCase):
|
|
16
|
+
def test_template_diagnostics(self):
|
|
17
|
+
raw = FakeClient()
|
|
18
|
+
result = MailTemplatesClient(raw).diagnostics("tpl_123", stream="broadcast")
|
|
19
|
+
|
|
20
|
+
self.assertEqual(result["findings"][0]["code"], "remote_images")
|
|
21
|
+
self.assertEqual(raw.calls[0][0:2], ("POST", "/mail/templates/tpl_123/diagnostics"))
|
|
22
|
+
self.assertEqual(raw.calls[0][2]["json"]["stream"], "broadcast")
|
stackshift-0.1.2/tests/test_mail_phase7.py → stackshift-1.0.0/tests/test_mail_events_webhooks.py
RENAMED
|
@@ -34,7 +34,7 @@ class FakeSession:
|
|
|
34
34
|
if url.endswith("/mail/events/evt_123"):
|
|
35
35
|
return FakeResponse(200, event_response())
|
|
36
36
|
if url.endswith("/mail/messages/msg_123/timeline"):
|
|
37
|
-
return FakeResponse(200, {"data": [{"type": "mail.message.
|
|
37
|
+
return FakeResponse(200, {"data": [{"type": "mail.message.mta_accepted", "label": "Accepted by StackShift outbound MTA"}]})
|
|
38
38
|
if url.endswith("/mail/webhooks") and method == "POST":
|
|
39
39
|
payload = webhook_response()
|
|
40
40
|
payload["secret"] = "whsec_test"
|
|
@@ -60,22 +60,22 @@ class FakeSession:
|
|
|
60
60
|
return FakeResponse(404, {"error": {"code": "not_found", "message": "Not found."}})
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class
|
|
63
|
+
class MailEventsWebhooksSDKTest(unittest.TestCase):
|
|
64
64
|
def test_events_timeline_and_webhooks(self):
|
|
65
65
|
session = FakeSession()
|
|
66
66
|
stackshift = StackShift(api_key="sk_test", session=session)
|
|
67
67
|
|
|
68
|
-
events = stackshift.mail.events.list(type="mail.message.
|
|
68
|
+
events = stackshift.mail.events.list(type="mail.message.mta_accepted", message_id="msg_123", limit=20)
|
|
69
69
|
self.assertEqual(events["data"][0]["id"], "evt_123")
|
|
70
70
|
self.assertEqual(session.calls[0][2]["params"]["messageId"], "msg_123")
|
|
71
71
|
|
|
72
72
|
event = stackshift.mail.events.get("evt_123")
|
|
73
|
-
self.assertEqual(event["type"], "mail.message.
|
|
73
|
+
self.assertEqual(event["type"], "mail.message.mta_accepted")
|
|
74
74
|
|
|
75
75
|
timeline = stackshift.mail.messages.timeline("msg_123")
|
|
76
76
|
self.assertEqual(timeline["data"][0]["label"], "Accepted by StackShift outbound MTA")
|
|
77
77
|
|
|
78
|
-
created = stackshift.mail.webhooks.create(url="https://example.com/hook", event_types=["mail.message.
|
|
78
|
+
created = stackshift.mail.webhooks.create(url="https://example.com/hook", event_types=["mail.message.mta_accepted"])
|
|
79
79
|
self.assertEqual(created["secret"], "whsec_test")
|
|
80
80
|
|
|
81
81
|
webhooks = stackshift.mail.webhooks.list(limit=10)
|
|
@@ -140,9 +140,9 @@ class MailPhase7SDKTest(unittest.TestCase):
|
|
|
140
140
|
def event_response():
|
|
141
141
|
return {
|
|
142
142
|
"id": "evt_123",
|
|
143
|
-
"type": "mail.message.
|
|
143
|
+
"type": "mail.message.mta_accepted",
|
|
144
144
|
"messageId": "msg_123",
|
|
145
|
-
"payload": {"messageId": "msg_123", "status": "
|
|
145
|
+
"payload": {"messageId": "msg_123", "status": "mta_accepted"},
|
|
146
146
|
"occurredAt": "2026-05-09T12:00:00Z",
|
|
147
147
|
"createdAt": "2026-05-09T12:00:00Z",
|
|
148
148
|
}
|
|
@@ -154,7 +154,7 @@ def webhook_response():
|
|
|
154
154
|
"url": "https://example.com/hook",
|
|
155
155
|
"description": None,
|
|
156
156
|
"status": "active",
|
|
157
|
-
"eventTypes": ["mail.message.
|
|
157
|
+
"eventTypes": ["mail.message.mta_accepted"],
|
|
158
158
|
"createdAt": "2026-05-09T12:00:00Z",
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from stackshift.mail_exports import MailExportsClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FakeClient:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.calls = []
|
|
10
|
+
|
|
11
|
+
def mail_request(self, method, path, **kwargs):
|
|
12
|
+
self.calls.append((method, path, kwargs))
|
|
13
|
+
return {"id": "exp_123", "status": "queued"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MailExportsTest(unittest.TestCase):
|
|
17
|
+
def test_create_list_get_and_download_path(self):
|
|
18
|
+
raw = FakeClient()
|
|
19
|
+
exports = MailExportsClient(raw)
|
|
20
|
+
created = exports.create(from_=datetime(2026, 8, 1, tzinfo=timezone.utc))
|
|
21
|
+
exports.list()
|
|
22
|
+
exports.get("exp_123")
|
|
23
|
+
|
|
24
|
+
self.assertEqual(created["id"], "exp_123")
|
|
25
|
+
self.assertEqual(raw.calls[0][2]["json"]["kind"], "recipient_delivery")
|
|
26
|
+
self.assertEqual(exports.download_path("exp_123"), "/mail/exports/exp_123/download")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|