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.
Files changed (25) hide show
  1. {stackshift-0.1.2 → stackshift-1.0.0}/PKG-INFO +1 -1
  2. {stackshift-0.1.2 → stackshift-1.0.0}/README.md +2 -2
  3. {stackshift-0.1.2 → stackshift-1.0.0}/pyproject.toml +1 -1
  4. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/__init__.py +8 -1
  5. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/client.py +1 -1
  6. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail.py +35 -3
  7. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail_advanced.py +21 -1
  8. stackshift-1.0.0/stackshift/mail_campaigns.py +61 -0
  9. stackshift-0.1.2/stackshift/mail_phase7.py → stackshift-1.0.0/stackshift/mail_events_webhooks.py +8 -0
  10. stackshift-1.0.0/stackshift/mail_exports.py +30 -0
  11. stackshift-1.0.0/stackshift/mail_streams.py +18 -0
  12. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/PKG-INFO +1 -1
  13. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/SOURCES.txt +7 -2
  14. {stackshift-0.1.2 → stackshift-1.0.0}/tests/test_mail.py +7 -7
  15. {stackshift-0.1.2 → stackshift-1.0.0}/tests/test_mail_advanced.py +11 -2
  16. stackshift-1.0.0/tests/test_mail_diagnostics.py +22 -0
  17. stackshift-0.1.2/tests/test_mail_phase7.py → stackshift-1.0.0/tests/test_mail_events_webhooks.py +8 -8
  18. stackshift-1.0.0/tests/test_mail_exports.py +30 -0
  19. {stackshift-0.1.2 → stackshift-1.0.0}/setup.cfg +0 -0
  20. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/assets.py +0 -0
  21. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/mail_reputation.py +0 -0
  22. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift/projects.py +0 -0
  23. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/dependency_links.txt +0 -0
  24. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/requires.txt +0 -0
  25. {stackshift-0.1.2 → stackshift-1.0.0}/stackshift.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: Official Python SDK for StackShift.
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: requests>=2.31
@@ -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="sent", limit=20)
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
- In this MVP, `sent` means the message was accepted by StackShift's outbound MTA. It does not mean inbox delivery, opens, clicks, or spam placement. A `sent` message can later become `bounced`.
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackshift"
7
- version = "0.1.2"
7
+ version = "1.0.0"
8
8
  description = "Official Python SDK for StackShift."
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["requests>=2.31"]
@@ -8,7 +8,10 @@ from .mail import (
8
8
  MailTemplatesClient,
9
9
  )
10
10
  from .mail_reputation import MailLimitsClient, MailReputationClient, MailReputationEventsClient
11
- from .mail_phase7 import MailEventsClient, MailWebhooksClient, verify_webhook_signature
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.1"
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 .mail_phase7 import MailEventsClient, MailWebhooksClient
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
- return self._client.mail_request("GET", f"/mail/messages/{quote(message_id)}/timeline")
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 = {"template": template, "from": from_, "recipients": recipients, "versionId": version_id}
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")
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: Official Python SDK for StackShift.
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: requests>=2.31
@@ -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/mail_phase7.py
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/test_mail_phase7.py
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": "sent"})
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": "sent"}], "nextCursor": None})
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="sent", limit=20)
156
- self.assertEqual(messages["data"][0]["status"], "sent")
157
- self.assertEqual(session.calls[1][2]["params"]["status"], "sent")
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"], "sent")
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.email ~all",
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": {"sent": 1}, "series": [{"bucket": "2026-05-11", "sent": 1}]})
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"]["sent"], 1)
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")
@@ -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.sent", "label": "Accepted by StackShift outbound MTA"}]})
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 MailPhase7SDKTest(unittest.TestCase):
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.sent", message_id="msg_123", limit=20)
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.sent")
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.sent"])
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.sent",
143
+ "type": "mail.message.mta_accepted",
144
144
  "messageId": "msg_123",
145
- "payload": {"messageId": "msg_123", "status": "sent"},
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.sent"],
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