zasend 0.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.
zasend-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zaSend
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
zasend-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: zasend
3
+ Version: 0.1.0
4
+ Summary: Python transactional email API client for zaSend
5
+ Author-email: zaSend <support@zasend.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://zasend.com
8
+ Project-URL: Documentation, https://zasend.com/docs
9
+ Project-URL: Repository, https://github.com/lr2bmail/firemail-api/tree/master/clients/python
10
+ Project-URL: Issues, https://github.com/lr2bmail/firemail-api/issues
11
+ Project-URL: Changelog, https://github.com/lr2bmail/firemail-api/commits/master/clients/python
12
+ Keywords: email,transactional-email,email-api,smtp,python-email,django-email,flask-email,fastapi-email,webhooks,dkim,zasend
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Communications :: Email
24
+ Classifier: Topic :: Communications :: Email :: Mail Transport Agents
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: requests>=2.25
31
+ Dynamic: license-file
32
+
33
+ # zasend
34
+
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
36
+ [![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)](pyproject.toml)
37
+
38
+ Official Python client for the [zaSend](https://zasend.com) transactional email API.
39
+
40
+ zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
41
+
42
+ Use it to:
43
+
44
+ - Send transactional email from verified domains
45
+ - Send template-based email with variables
46
+ - Check message status and events
47
+ - Manage suppressions, webhooks, templates, and domains
48
+ - Verify zaSend webhook signatures
49
+
50
+ The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install zasend
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ```python
61
+ from zasend import ZaSend
62
+
63
+ client = ZaSend(api_key="sk_live_...")
64
+
65
+ result = client.send_email(
66
+ from_email="zaSend <noreply@zasend.com>",
67
+ to="user@example.com",
68
+ subject="Welcome",
69
+ text="Thanks for signing up.",
70
+ )
71
+
72
+ print(result["message_id"])
73
+ ```
74
+
75
+ You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
76
+
77
+ ## Send HTML Email
78
+
79
+ ```python
80
+ client.send_email(
81
+ from_email="Acme <noreply@acme.com>",
82
+ to="customer@example.com",
83
+ subject="Your receipt",
84
+ html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
85
+ text="Thanks for your order. Your receipt is attached.",
86
+ )
87
+ ```
88
+
89
+ ## Send to Multiple Recipients
90
+
91
+ ```python
92
+ client.send_email(
93
+ from_email="Acme <noreply@acme.com>",
94
+ to=["one@example.com", "two@example.com"],
95
+ subject="Product update",
96
+ text="A new update is available.",
97
+ )
98
+ ```
99
+
100
+ zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
101
+
102
+ ## Template Send
103
+
104
+ ```python
105
+ client.send_template_email(
106
+ from_email="Acme <noreply@acme.com>",
107
+ to="customer@example.com",
108
+ template="welcome",
109
+ variables={"name": "Ada"},
110
+ )
111
+ ```
112
+
113
+ ## Django Example
114
+
115
+ ```python
116
+ # settings.py
117
+ ZASEND_API_KEY = "sk_live_..."
118
+
119
+ # anywhere in your app
120
+ from django.conf import settings
121
+ from zasend import ZaSend
122
+
123
+ zasend = ZaSend(settings.ZASEND_API_KEY)
124
+ zasend.send_email(
125
+ from_email="Acme <noreply@acme.com>",
126
+ to=user.email,
127
+ subject="Reset your password",
128
+ text="Use this link to reset your password.",
129
+ )
130
+ ```
131
+
132
+ ## Flask or FastAPI Example
133
+
134
+ ```python
135
+ import os
136
+ from zasend import ZaSend
137
+
138
+ zasend = ZaSend(os.environ["ZASEND_API_KEY"])
139
+
140
+ def send_signup_email(email):
141
+ return zasend.send_template_email(
142
+ from_email="Acme <noreply@acme.com>",
143
+ to=email,
144
+ template="welcome",
145
+ variables={"product": "Acme"},
146
+ )
147
+ ```
148
+
149
+ ## Webhook Verification
150
+
151
+ zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
152
+
153
+ ```python
154
+ from zasend import verify_webhook_signature
155
+
156
+ ok = verify_webhook_signature(
157
+ request.get_data(),
158
+ "whsec_...",
159
+ request.headers.get("X-zaSend-Signature"),
160
+ )
161
+ ```
162
+
163
+ ## API Methods
164
+
165
+ | Method | Description |
166
+ | --- | --- |
167
+ | `send_email(**message)` | Send direct content or a template email |
168
+ | `send_template_email(...)` | Convenience wrapper for template sends |
169
+ | `get_email(message_id)` | Fetch email status and events |
170
+ | `get_rate_limits()` | Fetch daily usage and limits |
171
+ | `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
172
+ | `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
173
+ | `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
174
+ | `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
175
+
176
+ ## Error Handling
177
+
178
+ ```python
179
+ from zasend import APIError, RateLimitError, ValidationError, ZaSend
180
+
181
+ try:
182
+ ZaSend("sk_live_...").send_email(
183
+ from_email="Acme <noreply@acme.com>",
184
+ to="user@example.com",
185
+ subject="Hello",
186
+ text="Body",
187
+ )
188
+ except RateLimitError as exc:
189
+ print("Rate limited:", exc)
190
+ except ValidationError as exc:
191
+ print("Invalid request:", exc)
192
+ except APIError as exc:
193
+ print("zaSend API error:", exc)
194
+ ```
195
+
196
+ ## Links
197
+
198
+ - Website: [zasend.com](https://zasend.com)
199
+ - API documentation: [zasend.com/docs](https://zasend.com/docs)
200
+ - Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
201
+
202
+ ## License
203
+
204
+ MIT
zasend-0.1.0/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # zasend
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
4
+ [![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)](pyproject.toml)
5
+
6
+ Official Python client for the [zaSend](https://zasend.com) transactional email API.
7
+
8
+ zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
9
+
10
+ Use it to:
11
+
12
+ - Send transactional email from verified domains
13
+ - Send template-based email with variables
14
+ - Check message status and events
15
+ - Manage suppressions, webhooks, templates, and domains
16
+ - Verify zaSend webhook signatures
17
+
18
+ The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install zasend
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ from zasend import ZaSend
30
+
31
+ client = ZaSend(api_key="sk_live_...")
32
+
33
+ result = client.send_email(
34
+ from_email="zaSend <noreply@zasend.com>",
35
+ to="user@example.com",
36
+ subject="Welcome",
37
+ text="Thanks for signing up.",
38
+ )
39
+
40
+ print(result["message_id"])
41
+ ```
42
+
43
+ You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
44
+
45
+ ## Send HTML Email
46
+
47
+ ```python
48
+ client.send_email(
49
+ from_email="Acme <noreply@acme.com>",
50
+ to="customer@example.com",
51
+ subject="Your receipt",
52
+ html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
53
+ text="Thanks for your order. Your receipt is attached.",
54
+ )
55
+ ```
56
+
57
+ ## Send to Multiple Recipients
58
+
59
+ ```python
60
+ client.send_email(
61
+ from_email="Acme <noreply@acme.com>",
62
+ to=["one@example.com", "two@example.com"],
63
+ subject="Product update",
64
+ text="A new update is available.",
65
+ )
66
+ ```
67
+
68
+ zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
69
+
70
+ ## Template Send
71
+
72
+ ```python
73
+ client.send_template_email(
74
+ from_email="Acme <noreply@acme.com>",
75
+ to="customer@example.com",
76
+ template="welcome",
77
+ variables={"name": "Ada"},
78
+ )
79
+ ```
80
+
81
+ ## Django Example
82
+
83
+ ```python
84
+ # settings.py
85
+ ZASEND_API_KEY = "sk_live_..."
86
+
87
+ # anywhere in your app
88
+ from django.conf import settings
89
+ from zasend import ZaSend
90
+
91
+ zasend = ZaSend(settings.ZASEND_API_KEY)
92
+ zasend.send_email(
93
+ from_email="Acme <noreply@acme.com>",
94
+ to=user.email,
95
+ subject="Reset your password",
96
+ text="Use this link to reset your password.",
97
+ )
98
+ ```
99
+
100
+ ## Flask or FastAPI Example
101
+
102
+ ```python
103
+ import os
104
+ from zasend import ZaSend
105
+
106
+ zasend = ZaSend(os.environ["ZASEND_API_KEY"])
107
+
108
+ def send_signup_email(email):
109
+ return zasend.send_template_email(
110
+ from_email="Acme <noreply@acme.com>",
111
+ to=email,
112
+ template="welcome",
113
+ variables={"product": "Acme"},
114
+ )
115
+ ```
116
+
117
+ ## Webhook Verification
118
+
119
+ zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
120
+
121
+ ```python
122
+ from zasend import verify_webhook_signature
123
+
124
+ ok = verify_webhook_signature(
125
+ request.get_data(),
126
+ "whsec_...",
127
+ request.headers.get("X-zaSend-Signature"),
128
+ )
129
+ ```
130
+
131
+ ## API Methods
132
+
133
+ | Method | Description |
134
+ | --- | --- |
135
+ | `send_email(**message)` | Send direct content or a template email |
136
+ | `send_template_email(...)` | Convenience wrapper for template sends |
137
+ | `get_email(message_id)` | Fetch email status and events |
138
+ | `get_rate_limits()` | Fetch daily usage and limits |
139
+ | `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
140
+ | `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
141
+ | `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
142
+ | `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
143
+
144
+ ## Error Handling
145
+
146
+ ```python
147
+ from zasend import APIError, RateLimitError, ValidationError, ZaSend
148
+
149
+ try:
150
+ ZaSend("sk_live_...").send_email(
151
+ from_email="Acme <noreply@acme.com>",
152
+ to="user@example.com",
153
+ subject="Hello",
154
+ text="Body",
155
+ )
156
+ except RateLimitError as exc:
157
+ print("Rate limited:", exc)
158
+ except ValidationError as exc:
159
+ print("Invalid request:", exc)
160
+ except APIError as exc:
161
+ print("zaSend API error:", exc)
162
+ ```
163
+
164
+ ## Links
165
+
166
+ - Website: [zasend.com](https://zasend.com)
167
+ - API documentation: [zasend.com/docs](https://zasend.com/docs)
168
+ - Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zasend"
7
+ version = "0.1.0"
8
+ description = "Python transactional email API client for zaSend"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "zaSend", email = "support@zasend.com"},
14
+ ]
15
+ keywords = [
16
+ "email",
17
+ "transactional-email",
18
+ "email-api",
19
+ "smtp",
20
+ "python-email",
21
+ "django-email",
22
+ "flask-email",
23
+ "fastapi-email",
24
+ "webhooks",
25
+ "dkim",
26
+ "zasend",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 3 - Alpha",
30
+ "Intended Audience :: Developers",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.8",
34
+ "Programming Language :: Python :: 3.9",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Programming Language :: Python :: 3.13",
39
+ "Topic :: Communications :: Email",
40
+ "Topic :: Communications :: Email :: Mail Transport Agents",
41
+ "Topic :: Internet :: WWW/HTTP",
42
+ "Topic :: Software Development :: Libraries :: Python Modules",
43
+ ]
44
+ dependencies = [
45
+ "requests>=2.25",
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://zasend.com"
50
+ Documentation = "https://zasend.com/docs"
51
+ Repository = "https://github.com/lr2bmail/firemail-api/tree/master/clients/python"
52
+ Issues = "https://github.com/lr2bmail/firemail-api/issues"
53
+ Changelog = "https://github.com/lr2bmail/firemail-api/commits/master/clients/python"
54
+
55
+ [tool.setuptools.packages.find]
56
+ include = ["zasend*"]
zasend-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,91 @@
1
+ import hashlib
2
+ import hmac
3
+
4
+ import pytest
5
+
6
+ from zasend import APIError, RateLimitError, ValidationError, ZaSend, ZaSendError, verify_webhook_signature
7
+
8
+
9
+ class Response:
10
+ def __init__(self, body=None, status_code=200, ok=True, reason="OK"):
11
+ self._body = body
12
+ self.status_code = status_code
13
+ self.ok = ok
14
+ self.reason = reason
15
+
16
+ def json(self):
17
+ if isinstance(self._body, Exception):
18
+ raise self._body
19
+ return self._body
20
+
21
+
22
+ class Session:
23
+ def __init__(self, response):
24
+ self.response = response
25
+ self.calls = []
26
+
27
+ def request(self, *args, **kwargs):
28
+ self.calls.append((args, kwargs))
29
+ return self.response
30
+
31
+
32
+ def test_send_email_posts_json_with_bearer_auth():
33
+ session = Session(Response({"success": True, "message_id": "msg_123"}))
34
+ client = ZaSend("key", base_url="https://example.test/api/v1", session=session)
35
+
36
+ result = client.send_email(
37
+ **{
38
+ "from": "noreply@example.com",
39
+ "to": "user@example.com",
40
+ "subject": "Hello",
41
+ "text": "Body",
42
+ }
43
+ )
44
+
45
+ args, kwargs = session.calls[0]
46
+ assert args == ("POST", "https://example.test/api/v1/emails/send")
47
+ assert kwargs["headers"]["Authorization"] == "Bearer key"
48
+ assert kwargs["json"]["subject"] == "Hello"
49
+ assert result["message_id"] == "msg_123"
50
+
51
+
52
+ def test_list_suppressions_sends_query_parameters():
53
+ session = Session(Response({"suppressions": []}))
54
+ client = ZaSend("key", base_url="https://example.test/api/v1", session=session)
55
+
56
+ client.list_suppressions(page=2, per_page=100, reason="bounce")
57
+
58
+ _, kwargs = session.calls[0]
59
+ assert kwargs["params"] == {"page": 2, "per_page": 100, "reason": "bounce"}
60
+
61
+
62
+ def test_typed_errors():
63
+ with pytest.raises(RateLimitError):
64
+ ZaSend("key", session=Session(Response({"message": "slow down"}, 429, False))).get_rate_limits()
65
+
66
+ with pytest.raises(ValidationError):
67
+ ZaSend("key", session=Session(Response({"message": "bad email"}, 422, False))).send_email()
68
+
69
+ with pytest.raises(APIError):
70
+ ZaSend("key", session=Session(Response({"message": "missing"}, 404, False))).get_email("missing")
71
+
72
+
73
+ def test_delete_methods_return_none_for_204():
74
+ client = ZaSend("key", session=Session(Response(None, 204, True)))
75
+
76
+ assert client.delete_webhook("hook_123") is None
77
+
78
+
79
+ def test_webhook_signature_verification_accepts_valid_signature():
80
+ body = b'{"event":"accepted_by_postfix"}'
81
+ secret = "secret"
82
+ signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
83
+
84
+ assert verify_webhook_signature(body, secret, "sha256={0}".format(signature))
85
+
86
+
87
+ def test_client_errors_still_behave_like_value_error():
88
+ assert issubclass(APIError, ZaSendError)
89
+ assert issubclass(RateLimitError, APIError)
90
+ assert issubclass(ValidationError, APIError)
91
+ assert issubclass(ZaSendError, ValueError)
@@ -0,0 +1,12 @@
1
+ from .client import APIError, RateLimitError, ValidationError, ZaSend, ZaSendError, verify_webhook_signature
2
+
3
+ __all__ = [
4
+ "APIError",
5
+ "RateLimitError",
6
+ "ValidationError",
7
+ "ZaSend",
8
+ "ZaSendError",
9
+ "verify_webhook_signature",
10
+ ]
11
+
12
+ __version__ = "0.1.0"
@@ -0,0 +1,171 @@
1
+ import hashlib
2
+ import hmac
3
+
4
+ import requests
5
+
6
+
7
+ DEFAULT_BASE_URL = "https://zasend.com/api/v1"
8
+
9
+
10
+ class ZaSendError(ValueError):
11
+ """Base exception for zaSend client errors."""
12
+
13
+
14
+ class APIError(ZaSendError):
15
+ """Raised when the zaSend API returns an error response."""
16
+
17
+ def __init__(self, message, status_code=None, body=None):
18
+ super().__init__(message)
19
+ self.status_code = status_code
20
+ self.body = body
21
+
22
+
23
+ class RateLimitError(APIError):
24
+ """Raised when the API returns 429."""
25
+
26
+
27
+ class ValidationError(APIError):
28
+ """Raised when the API returns a validation-style error."""
29
+
30
+
31
+ class ZaSend:
32
+ """Lightweight client for the zaSend API."""
33
+
34
+ def __init__(self, api_key, base_url=None, timeout=10, session=None):
35
+ if not api_key:
36
+ raise ZaSendError("API key is required")
37
+ self.api_key = api_key
38
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
39
+ self.timeout = timeout
40
+ self._session = session or requests.Session()
41
+
42
+ def send_email(self, **message):
43
+ if "from_email" in message and "from" not in message:
44
+ message["from"] = message.pop("from_email")
45
+ return self._request("POST", "/emails/send", json=message)
46
+
47
+ def send_template_email(
48
+ self,
49
+ from_email,
50
+ to,
51
+ template,
52
+ variables=None,
53
+ cc=None,
54
+ bcc=None,
55
+ list_unsubscribe=None,
56
+ list_unsubscribe_post=None,
57
+ ):
58
+ return self.send_email(
59
+ **{
60
+ "from": from_email,
61
+ "to": to,
62
+ "template": template,
63
+ "variables": variables or {},
64
+ "cc": cc,
65
+ "bcc": bcc,
66
+ "list_unsubscribe": list_unsubscribe,
67
+ "list_unsubscribe_post": list_unsubscribe_post,
68
+ }
69
+ )
70
+
71
+ def get_email(self, message_id):
72
+ return self._request("GET", "/emails/{0}".format(message_id))
73
+
74
+ def get_rate_limits(self):
75
+ return self._request("GET", "/rate-limits")
76
+
77
+ def list_domains(self):
78
+ return self._request("GET", "/domains")
79
+
80
+ def add_domain(self, domain):
81
+ return self._request("POST", "/domains", json={"domain": domain})
82
+
83
+ def verify_domain(self, domain_id):
84
+ return self._request("POST", "/domains/{0}/verify".format(domain_id))
85
+
86
+ def delete_domain(self, domain_id):
87
+ return self._request("DELETE", "/domains/{0}".format(domain_id))
88
+
89
+ def list_suppressions(self, page=None, per_page=None, reason=None):
90
+ return self._request(
91
+ "GET",
92
+ "/suppressions",
93
+ params={"page": page, "per_page": per_page, "reason": reason},
94
+ )
95
+
96
+ def add_suppression(self, email, reason="manual", details=None):
97
+ return self._request(
98
+ "POST",
99
+ "/suppressions",
100
+ json={"email": email, "reason": reason, "details": details},
101
+ )
102
+
103
+ def delete_suppression(self, suppression_id):
104
+ return self._request("DELETE", "/suppressions/{0}".format(suppression_id))
105
+
106
+ def list_webhooks(self):
107
+ return self._request("GET", "/webhooks")
108
+
109
+ def create_webhook(self, url, events):
110
+ return self._request("POST", "/webhooks", json={"url": url, "events": events})
111
+
112
+ def delete_webhook(self, webhook_id):
113
+ return self._request("DELETE", "/webhooks/{0}".format(webhook_id))
114
+
115
+ def list_templates(self):
116
+ return self._request("GET", "/templates")
117
+
118
+ def create_template(self, **template):
119
+ return self._request("POST", "/templates", json=template)
120
+
121
+ def get_template(self, template_id):
122
+ return self._request("GET", "/templates/{0}".format(template_id))
123
+
124
+ def update_template(self, template_id, **template):
125
+ return self._request("PUT", "/templates/{0}".format(template_id), json=template)
126
+
127
+ def delete_template(self, template_id):
128
+ return self._request("DELETE", "/templates/{0}".format(template_id))
129
+
130
+ def _request(self, method, path, json=None, params=None):
131
+ headers = {
132
+ "Authorization": "Bearer {0}".format(self.api_key),
133
+ "Accept": "application/json",
134
+ }
135
+ clean_params = {k: v for k, v in (params or {}).items() if v is not None}
136
+ response = self._session.request(
137
+ method,
138
+ "{0}{1}".format(self.base_url, path),
139
+ headers=headers,
140
+ json={k: v for k, v in (json or {}).items() if v is not None} if json is not None else None,
141
+ params=clean_params or None,
142
+ timeout=self.timeout,
143
+ )
144
+
145
+ if response.status_code == 204:
146
+ return None
147
+
148
+ try:
149
+ data = response.json()
150
+ except ValueError as exc:
151
+ if response.ok:
152
+ raise APIError("API returned invalid JSON", response.status_code) from exc
153
+ data = None
154
+
155
+ if not response.ok:
156
+ message = data.get("message") if isinstance(data, dict) else response.reason
157
+ if response.status_code == 429:
158
+ raise RateLimitError(message, response.status_code, data)
159
+ if response.status_code in (400, 422):
160
+ raise ValidationError(message, response.status_code, data)
161
+ raise APIError("API error {0}: {1}".format(response.status_code, message), response.status_code, data)
162
+
163
+ return data
164
+
165
+
166
+ def verify_webhook_signature(payload_body, secret, signature_header):
167
+ received = (signature_header or "").replace("sha256=", "", 1)
168
+ if isinstance(payload_body, str):
169
+ payload_body = payload_body.encode()
170
+ expected = hmac.new(secret.encode(), payload_body, hashlib.sha256).hexdigest()
171
+ return hmac.compare_digest(expected, received)
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: zasend
3
+ Version: 0.1.0
4
+ Summary: Python transactional email API client for zaSend
5
+ Author-email: zaSend <support@zasend.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://zasend.com
8
+ Project-URL: Documentation, https://zasend.com/docs
9
+ Project-URL: Repository, https://github.com/lr2bmail/firemail-api/tree/master/clients/python
10
+ Project-URL: Issues, https://github.com/lr2bmail/firemail-api/issues
11
+ Project-URL: Changelog, https://github.com/lr2bmail/firemail-api/commits/master/clients/python
12
+ Keywords: email,transactional-email,email-api,smtp,python-email,django-email,flask-email,fastapi-email,webhooks,dkim,zasend
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Communications :: Email
24
+ Classifier: Topic :: Communications :: Email :: Mail Transport Agents
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: requests>=2.25
31
+ Dynamic: license-file
32
+
33
+ # zasend
34
+
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
36
+ [![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)](pyproject.toml)
37
+
38
+ Official Python client for the [zaSend](https://zasend.com) transactional email API.
39
+
40
+ zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
41
+
42
+ Use it to:
43
+
44
+ - Send transactional email from verified domains
45
+ - Send template-based email with variables
46
+ - Check message status and events
47
+ - Manage suppressions, webhooks, templates, and domains
48
+ - Verify zaSend webhook signatures
49
+
50
+ The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install zasend
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ```python
61
+ from zasend import ZaSend
62
+
63
+ client = ZaSend(api_key="sk_live_...")
64
+
65
+ result = client.send_email(
66
+ from_email="zaSend <noreply@zasend.com>",
67
+ to="user@example.com",
68
+ subject="Welcome",
69
+ text="Thanks for signing up.",
70
+ )
71
+
72
+ print(result["message_id"])
73
+ ```
74
+
75
+ You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
76
+
77
+ ## Send HTML Email
78
+
79
+ ```python
80
+ client.send_email(
81
+ from_email="Acme <noreply@acme.com>",
82
+ to="customer@example.com",
83
+ subject="Your receipt",
84
+ html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
85
+ text="Thanks for your order. Your receipt is attached.",
86
+ )
87
+ ```
88
+
89
+ ## Send to Multiple Recipients
90
+
91
+ ```python
92
+ client.send_email(
93
+ from_email="Acme <noreply@acme.com>",
94
+ to=["one@example.com", "two@example.com"],
95
+ subject="Product update",
96
+ text="A new update is available.",
97
+ )
98
+ ```
99
+
100
+ zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
101
+
102
+ ## Template Send
103
+
104
+ ```python
105
+ client.send_template_email(
106
+ from_email="Acme <noreply@acme.com>",
107
+ to="customer@example.com",
108
+ template="welcome",
109
+ variables={"name": "Ada"},
110
+ )
111
+ ```
112
+
113
+ ## Django Example
114
+
115
+ ```python
116
+ # settings.py
117
+ ZASEND_API_KEY = "sk_live_..."
118
+
119
+ # anywhere in your app
120
+ from django.conf import settings
121
+ from zasend import ZaSend
122
+
123
+ zasend = ZaSend(settings.ZASEND_API_KEY)
124
+ zasend.send_email(
125
+ from_email="Acme <noreply@acme.com>",
126
+ to=user.email,
127
+ subject="Reset your password",
128
+ text="Use this link to reset your password.",
129
+ )
130
+ ```
131
+
132
+ ## Flask or FastAPI Example
133
+
134
+ ```python
135
+ import os
136
+ from zasend import ZaSend
137
+
138
+ zasend = ZaSend(os.environ["ZASEND_API_KEY"])
139
+
140
+ def send_signup_email(email):
141
+ return zasend.send_template_email(
142
+ from_email="Acme <noreply@acme.com>",
143
+ to=email,
144
+ template="welcome",
145
+ variables={"product": "Acme"},
146
+ )
147
+ ```
148
+
149
+ ## Webhook Verification
150
+
151
+ zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
152
+
153
+ ```python
154
+ from zasend import verify_webhook_signature
155
+
156
+ ok = verify_webhook_signature(
157
+ request.get_data(),
158
+ "whsec_...",
159
+ request.headers.get("X-zaSend-Signature"),
160
+ )
161
+ ```
162
+
163
+ ## API Methods
164
+
165
+ | Method | Description |
166
+ | --- | --- |
167
+ | `send_email(**message)` | Send direct content or a template email |
168
+ | `send_template_email(...)` | Convenience wrapper for template sends |
169
+ | `get_email(message_id)` | Fetch email status and events |
170
+ | `get_rate_limits()` | Fetch daily usage and limits |
171
+ | `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
172
+ | `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
173
+ | `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
174
+ | `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
175
+
176
+ ## Error Handling
177
+
178
+ ```python
179
+ from zasend import APIError, RateLimitError, ValidationError, ZaSend
180
+
181
+ try:
182
+ ZaSend("sk_live_...").send_email(
183
+ from_email="Acme <noreply@acme.com>",
184
+ to="user@example.com",
185
+ subject="Hello",
186
+ text="Body",
187
+ )
188
+ except RateLimitError as exc:
189
+ print("Rate limited:", exc)
190
+ except ValidationError as exc:
191
+ print("Invalid request:", exc)
192
+ except APIError as exc:
193
+ print("zaSend API error:", exc)
194
+ ```
195
+
196
+ ## Links
197
+
198
+ - Website: [zasend.com](https://zasend.com)
199
+ - API documentation: [zasend.com/docs](https://zasend.com/docs)
200
+ - Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_client.py
5
+ zasend/__init__.py
6
+ zasend/client.py
7
+ zasend.egg-info/PKG-INFO
8
+ zasend.egg-info/SOURCES.txt
9
+ zasend.egg-info/dependency_links.txt
10
+ zasend.egg-info/requires.txt
11
+ zasend.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.25
@@ -0,0 +1 @@
1
+ zasend