mailblastr 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.
- mailblastr-1.0.0/LICENSE +21 -0
- mailblastr-1.0.0/PKG-INFO +298 -0
- mailblastr-1.0.0/README.md +271 -0
- mailblastr-1.0.0/mailblastr/__init__.py +74 -0
- mailblastr-1.0.0/mailblastr/api_keys.py +34 -0
- mailblastr-1.0.0/mailblastr/audiences.py +56 -0
- mailblastr-1.0.0/mailblastr/automations.py +98 -0
- mailblastr-1.0.0/mailblastr/batch.py +14 -0
- mailblastr-1.0.0/mailblastr/campaigns.py +110 -0
- mailblastr-1.0.0/mailblastr/contact_properties.py +50 -0
- mailblastr-1.0.0/mailblastr/contacts.py +191 -0
- mailblastr-1.0.0/mailblastr/domains.py +109 -0
- mailblastr-1.0.0/mailblastr/emails.py +180 -0
- mailblastr-1.0.0/mailblastr/events.py +52 -0
- mailblastr-1.0.0/mailblastr/exceptions.py +25 -0
- mailblastr-1.0.0/mailblastr/http_client.py +173 -0
- mailblastr-1.0.0/mailblastr/logs.py +41 -0
- mailblastr-1.0.0/mailblastr/polls.py +18 -0
- mailblastr-1.0.0/mailblastr/segments.py +76 -0
- mailblastr-1.0.0/mailblastr/templates.py +89 -0
- mailblastr-1.0.0/mailblastr/topics.py +71 -0
- mailblastr-1.0.0/mailblastr/webhooks.py +160 -0
- mailblastr-1.0.0/mailblastr.egg-info/PKG-INFO +298 -0
- mailblastr-1.0.0/mailblastr.egg-info/SOURCES.txt +34 -0
- mailblastr-1.0.0/mailblastr.egg-info/dependency_links.txt +1 -0
- mailblastr-1.0.0/mailblastr.egg-info/top_level.txt +1 -0
- mailblastr-1.0.0/pyproject.toml +35 -0
- mailblastr-1.0.0/setup.cfg +4 -0
- mailblastr-1.0.0/tests/test_campaigns.py +54 -0
- mailblastr-1.0.0/tests/test_contacts.py +108 -0
- mailblastr-1.0.0/tests/test_domains.py +48 -0
- mailblastr-1.0.0/tests/test_emails.py +111 -0
- mailblastr-1.0.0/tests/test_http_client.py +220 -0
- mailblastr-1.0.0/tests/test_misc_resources.py +150 -0
- mailblastr-1.0.0/tests/test_segments.py +53 -0
- mailblastr-1.0.0/tests/test_webhooks.py +119 -0
mailblastr-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MailBlastr
|
|
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.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailblastr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official MailBlastr Python SDK — send transactional and marketing email from your own verified domain.
|
|
5
|
+
Author: MailBlastr
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.mailblastr.com
|
|
8
|
+
Project-URL: Documentation, https://www.mailblastr.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/shekhu10/mailblastr-sdks
|
|
10
|
+
Keywords: mailblastr,email,transactional,marketing,smtp
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# mailblastr
|
|
29
|
+
|
|
30
|
+
Official Python SDK for the [MailBlastr](https://www.mailblastr.com) email API — send transactional and marketing email from your own verified domain.
|
|
31
|
+
|
|
32
|
+
Zero dependencies (Python standard library only). Python 3.8+.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install mailblastr
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
Grab your API key from the [MailBlastr dashboard](https://www.mailblastr.com).
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import mailblastr
|
|
46
|
+
|
|
47
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Send your first email
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import mailblastr
|
|
54
|
+
|
|
55
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
56
|
+
|
|
57
|
+
params: mailblastr.Emails.SendParams = {
|
|
58
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
59
|
+
"to": ["user@example.com"],
|
|
60
|
+
"subject": "Hello from MailBlastr",
|
|
61
|
+
"html": "<p>Your first email 🎉</p>",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
email = mailblastr.Emails.send(params)
|
|
65
|
+
print(email["id"])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Every method returns the parsed JSON response. On any non-2xx status the SDK raises `mailblastr.MailblastrError` carrying the API error body:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
try:
|
|
72
|
+
mailblastr.Emails.send(params)
|
|
73
|
+
except mailblastr.MailblastrError as e:
|
|
74
|
+
print(e.status_code, e.name, e.message) # e.g. 422 validation_error "..."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Attachments
|
|
78
|
+
|
|
79
|
+
Attach files by hosted URL (`path`, fetched at send time) or inline base64 (`content`):
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
mailblastr.Emails.send({
|
|
83
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
84
|
+
"to": ["user@example.com"],
|
|
85
|
+
"subject": "Your invoice",
|
|
86
|
+
"html": "<p>Invoice attached.</p>",
|
|
87
|
+
"attachments": [
|
|
88
|
+
{"filename": "invoice.pdf", "path": "https://yourdomain.com/invoices/invoice.pdf"},
|
|
89
|
+
{"filename": "report.csv", "content": base64_content, "content_type": "text/csv"},
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Batch send
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
mailblastr.Batch.send([
|
|
98
|
+
{"from": "hello@yourdomain.com", "to": ["a@example.com"], "subject": "Hi A", "html": "<p>A</p>"},
|
|
99
|
+
{"from": "hello@yourdomain.com", "to": ["b@example.com"], "subject": "Hi B", "html": "<p>B</p>"},
|
|
100
|
+
]) # up to 100 emails per request
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Options
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
mailblastr.base_url = "https://www.mailblastr.com/api" # override your API host
|
|
107
|
+
mailblastr.timeout = 30 # per-request timeout in seconds (default 30)
|
|
108
|
+
mailblastr.max_retries = 2 # auto-retry 429/503 responses (default 2; 0 disables)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Requests time out after 30 seconds by default. A `429` (rate limited) or `503`
|
|
112
|
+
(service unavailable) response is retried up to `max_retries` times, honoring the
|
|
113
|
+
`Retry-After` header (otherwise exponential backoff). Only those two statuses are
|
|
114
|
+
retried — never other errors, network failures, or timeouts — so a non-idempotent
|
|
115
|
+
request (like sending an email) is never duplicated by a retry.
|
|
116
|
+
|
|
117
|
+
## The domain-first model
|
|
118
|
+
|
|
119
|
+
MailBlastr is DOMAIN-FIRST: each of your verified sending domains has its own
|
|
120
|
+
contact pool — the same address on two domains is two records with separate
|
|
121
|
+
consent. That means:
|
|
122
|
+
|
|
123
|
+
- `Contacts` take a `domain` (required to create/list on the flat `/contacts`
|
|
124
|
+
API; disambiguates an email id on get/update/remove).
|
|
125
|
+
- `Segments` and `Topics` belong to a domain (`domain` required on create and list).
|
|
126
|
+
- `Campaigns.create` REQUIRES `domain` — it picks the contact pool the campaign
|
|
127
|
+
targets (the `from` address may be a different verified domain).
|
|
128
|
+
- `Automations.create` REQUIRES `domain`, and `Events.send` REQUIRES `domain` —
|
|
129
|
+
only automations belonging to that domain are triggered, so the same event
|
|
130
|
+
name (e.g. `user.created`) across several products can never double-fire.
|
|
131
|
+
|
|
132
|
+
## Resources
|
|
133
|
+
|
|
134
|
+
Each resource is a class with methods following a consistent
|
|
135
|
+
`create` / `get` / `list` / `update` / `remove` shape (plus resource-specific verbs):
|
|
136
|
+
`Emails` (with nested `Emails.Attachments` and `Emails.Receiving`), `Batch`,
|
|
137
|
+
`Domains`, `Audiences`, `Contacts`, `ContactProperties`, `Campaigns`,
|
|
138
|
+
`Segments`, `Topics`, `Templates`, `Automations`, `Webhooks`, `Events`,
|
|
139
|
+
`ApiKeys`, `Logs`, `Polls`.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Emails
|
|
143
|
+
mailblastr.Emails.send(params)
|
|
144
|
+
mailblastr.Emails.list({"limit": 20, "after": cursor}) # cursor pagination
|
|
145
|
+
mailblastr.Emails.get(email_id)
|
|
146
|
+
mailblastr.Emails.update(email_id, {"scheduled_at": "2026-08-01T09:00:00Z"}) # reschedule
|
|
147
|
+
mailblastr.Emails.cancel(email_id)
|
|
148
|
+
mailblastr.Emails.Attachments.list(email_id)
|
|
149
|
+
mailblastr.Emails.Attachments.get(email_id, attachment_id)
|
|
150
|
+
|
|
151
|
+
# Inbound email
|
|
152
|
+
mailblastr.Emails.Receiving.list()
|
|
153
|
+
mailblastr.Emails.Receiving.get(email_id)
|
|
154
|
+
mailblastr.Emails.Receiving.attachments(email_id)
|
|
155
|
+
mailblastr.Emails.Receiving.get_attachment(email_id, attachment_id) # -> bytes
|
|
156
|
+
mailblastr.Emails.Receiving.raw(email_id) # -> bytes (RFC822)
|
|
157
|
+
mailblastr.Emails.Receiving.forward(email_id, {"from": "me@yourdomain.com", "to": "team@you.com"})
|
|
158
|
+
mailblastr.Emails.Receiving.reply(email_id, {"from": "me@yourdomain.com", "html": "<p>Thanks!</p>"})
|
|
159
|
+
mailblastr.Emails.Receiving.remove(email_id)
|
|
160
|
+
|
|
161
|
+
# Domains (incl. claiming a domain verified elsewhere + one-click DNS)
|
|
162
|
+
mailblastr.Domains.create({"name": "yourdomain.com"})
|
|
163
|
+
mailblastr.Domains.verify(domain_id)
|
|
164
|
+
mailblastr.Domains.claim({"name": "yourdomain.com"})
|
|
165
|
+
mailblastr.Domains.verify_claim(domain_id)
|
|
166
|
+
mailblastr.Domains.detect_dns(domain_id)
|
|
167
|
+
mailblastr.Domains.apply_cloudflare_dns(domain_id, {"token": cf_token})
|
|
168
|
+
|
|
169
|
+
# Contacts (domain-first)
|
|
170
|
+
mailblastr.Contacts.create({"domain": "yourdomain.com", "email": "user@example.com", "first_name": "Ada"})
|
|
171
|
+
mailblastr.Contacts.list({"domain": "yourdomain.com"})
|
|
172
|
+
mailblastr.Contacts.get({"id": contact_id}) # by id (exact) …
|
|
173
|
+
mailblastr.Contacts.get({"id": "user@example.com", "domain": "yourdomain.com"}) # … or by email + domain
|
|
174
|
+
mailblastr.Contacts.update({"id": contact_id, "unsubscribed": True})
|
|
175
|
+
mailblastr.Contacts.remove({"id": contact_id})
|
|
176
|
+
mailblastr.Contacts.batch({"audience_id": aud_id, "contacts": [{"email": "a@b.com"}]})
|
|
177
|
+
mailblastr.Contacts.import_csv({"audience_id": aud_id, "csv": "email,company\na@b.com,Acme"})
|
|
178
|
+
mailblastr.Contacts.add_to_segment(contact_id, segment_id)
|
|
179
|
+
mailblastr.Contacts.list_segments(contact_id)
|
|
180
|
+
mailblastr.Contacts.update_topics(contact_id, {"topics": [{"id": topic_id, "subscription": "opt_in"}]})
|
|
181
|
+
|
|
182
|
+
# Contact properties (custom fields / merge tags)
|
|
183
|
+
mailblastr.ContactProperties.create({"key": "plan", "type": "string"})
|
|
184
|
+
|
|
185
|
+
# Campaigns & Segments (domain-first)
|
|
186
|
+
mailblastr.Campaigns.create({"domain": "yourdomain.com", "from": sender, "subject": subject, "html": html})
|
|
187
|
+
mailblastr.Campaigns.send(campaign_id, {"scheduled_at": "tomorrow 9am"})
|
|
188
|
+
mailblastr.Campaigns.stats(campaign_id)
|
|
189
|
+
mailblastr.Campaigns.ab(campaign_id)
|
|
190
|
+
mailblastr.Segments.create({"domain": "yourdomain.com", "name": "VIP", "filter": {"status": "subscribed"}})
|
|
191
|
+
mailblastr.Segments.list({"domain": "yourdomain.com"})
|
|
192
|
+
mailblastr.Segments.contacts(segment_id) # preview who matches
|
|
193
|
+
|
|
194
|
+
# Topics (domain-first)
|
|
195
|
+
mailblastr.Topics.create({"domain": "yourdomain.com", "name": "Product updates", "default_subscription": "opt_in"})
|
|
196
|
+
mailblastr.Topics.list({"domain": "yourdomain.com"})
|
|
197
|
+
|
|
198
|
+
# Templates
|
|
199
|
+
mailblastr.Templates.create({"name": "Welcome", "subject": "Hi {{first_name}}", "html": html})
|
|
200
|
+
mailblastr.Templates.duplicate(template_id)
|
|
201
|
+
mailblastr.Templates.publish(template_id)
|
|
202
|
+
mailblastr.Emails.send({"from": sender, "to": to, "template_id": tmpl_id, "variables": {"first_name": "Ada"}})
|
|
203
|
+
|
|
204
|
+
# Audiences
|
|
205
|
+
mailblastr.Audiences.list()
|
|
206
|
+
mailblastr.Audiences.import_sheet(audience_id, {"url": sheet_url})
|
|
207
|
+
|
|
208
|
+
# API keys
|
|
209
|
+
mailblastr.ApiKeys.create({"name": "CI", "permission": "sending_access"})
|
|
210
|
+
mailblastr.ApiKeys.list()
|
|
211
|
+
mailblastr.ApiKeys.remove(key_id)
|
|
212
|
+
|
|
213
|
+
# Logs & Polls
|
|
214
|
+
mailblastr.Logs.list({"limit": 100, "method": "POST", "status": 429})
|
|
215
|
+
mailblastr.Logs.get(log_id)
|
|
216
|
+
mailblastr.Polls.list()
|
|
217
|
+
mailblastr.Polls.get(email_id)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Automations & Events
|
|
221
|
+
|
|
222
|
+
Every automation belongs to one of your sending domains — `domain` is required
|
|
223
|
+
on create, and `Events.send` names the domain it targets.
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
automation = mailblastr.Automations.create({
|
|
227
|
+
"name": "Welcome series",
|
|
228
|
+
"domain": "yourdomain.com",
|
|
229
|
+
"trigger": "contact.created",
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
mailblastr.Automations.add_step(automation["id"], {
|
|
233
|
+
"type": "send_email",
|
|
234
|
+
"config": {"template_id": "tmpl_welcome"},
|
|
235
|
+
})
|
|
236
|
+
mailblastr.Automations.update(automation["id"], {"status": "enabled"})
|
|
237
|
+
|
|
238
|
+
# Fire a custom event — only yourdomain.com's automations are triggered
|
|
239
|
+
mailblastr.Events.send({
|
|
240
|
+
"event": "signup.completed",
|
|
241
|
+
"domain": "yourdomain.com",
|
|
242
|
+
"email": "user@example.com",
|
|
243
|
+
"payload": {"plan": "pro"},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
# Inspect execution
|
|
247
|
+
runs = mailblastr.Automations.runs(automation["id"], {"limit": 25})
|
|
248
|
+
mailblastr.Automations.get_run(automation["id"], runs["data"][0]["id"])
|
|
249
|
+
mailblastr.Automations.stop(automation["id"])
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Webhooks
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
hook = mailblastr.Webhooks.create({
|
|
256
|
+
"endpoint": "https://yourapp.com/hooks/mailblastr",
|
|
257
|
+
"events": ["email.delivered", "email.bounced", "contact.unsubscribed"],
|
|
258
|
+
})
|
|
259
|
+
signing_secret = hook["signing_secret"] # shown ONCE, only here
|
|
260
|
+
|
|
261
|
+
mailblastr.Webhooks.list()
|
|
262
|
+
mailblastr.Webhooks.update(hook["id"], {"status": "disabled"})
|
|
263
|
+
mailblastr.Webhooks.rotate(hook["id"]) # new secret, returned once
|
|
264
|
+
mailblastr.Webhooks.test(hook["id"])
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Verify incoming deliveries locally (no HTTP request) — pass the EXACT raw
|
|
268
|
+
request body string, the `svix-*` headers, and your signing secret:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
result = mailblastr.Webhooks.verify(raw_body, request.headers, signing_secret)
|
|
272
|
+
if not result["valid"]:
|
|
273
|
+
abort(401) # result["reason"] says why, e.g. 'no_match'
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Pagination
|
|
277
|
+
|
|
278
|
+
`list()` methods accept optional cursor pagination — `{"limit", "after", "before"}`:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
mailblastr.Campaigns.list({"limit": 25, "after": "cursor_abc"})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Idempotency
|
|
285
|
+
|
|
286
|
+
Pass an idempotency key to safely retry a create (24h window):
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
mailblastr.Emails.send(params, options={"idempotency_key": "order-123"})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Documentation
|
|
293
|
+
|
|
294
|
+
Full docs: <https://www.mailblastr.com/docs>
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# mailblastr
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [MailBlastr](https://www.mailblastr.com) email API — send transactional and marketing email from your own verified domain.
|
|
4
|
+
|
|
5
|
+
Zero dependencies (Python standard library only). Python 3.8+.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install mailblastr
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Grab your API key from the [MailBlastr dashboard](https://www.mailblastr.com).
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import mailblastr
|
|
19
|
+
|
|
20
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Send your first email
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import mailblastr
|
|
27
|
+
|
|
28
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
29
|
+
|
|
30
|
+
params: mailblastr.Emails.SendParams = {
|
|
31
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
32
|
+
"to": ["user@example.com"],
|
|
33
|
+
"subject": "Hello from MailBlastr",
|
|
34
|
+
"html": "<p>Your first email 🎉</p>",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
email = mailblastr.Emails.send(params)
|
|
38
|
+
print(email["id"])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every method returns the parsed JSON response. On any non-2xx status the SDK raises `mailblastr.MailblastrError` carrying the API error body:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
try:
|
|
45
|
+
mailblastr.Emails.send(params)
|
|
46
|
+
except mailblastr.MailblastrError as e:
|
|
47
|
+
print(e.status_code, e.name, e.message) # e.g. 422 validation_error "..."
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Attachments
|
|
51
|
+
|
|
52
|
+
Attach files by hosted URL (`path`, fetched at send time) or inline base64 (`content`):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
mailblastr.Emails.send({
|
|
56
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
57
|
+
"to": ["user@example.com"],
|
|
58
|
+
"subject": "Your invoice",
|
|
59
|
+
"html": "<p>Invoice attached.</p>",
|
|
60
|
+
"attachments": [
|
|
61
|
+
{"filename": "invoice.pdf", "path": "https://yourdomain.com/invoices/invoice.pdf"},
|
|
62
|
+
{"filename": "report.csv", "content": base64_content, "content_type": "text/csv"},
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Batch send
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
mailblastr.Batch.send([
|
|
71
|
+
{"from": "hello@yourdomain.com", "to": ["a@example.com"], "subject": "Hi A", "html": "<p>A</p>"},
|
|
72
|
+
{"from": "hello@yourdomain.com", "to": ["b@example.com"], "subject": "Hi B", "html": "<p>B</p>"},
|
|
73
|
+
]) # up to 100 emails per request
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Options
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
mailblastr.base_url = "https://www.mailblastr.com/api" # override your API host
|
|
80
|
+
mailblastr.timeout = 30 # per-request timeout in seconds (default 30)
|
|
81
|
+
mailblastr.max_retries = 2 # auto-retry 429/503 responses (default 2; 0 disables)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requests time out after 30 seconds by default. A `429` (rate limited) or `503`
|
|
85
|
+
(service unavailable) response is retried up to `max_retries` times, honoring the
|
|
86
|
+
`Retry-After` header (otherwise exponential backoff). Only those two statuses are
|
|
87
|
+
retried — never other errors, network failures, or timeouts — so a non-idempotent
|
|
88
|
+
request (like sending an email) is never duplicated by a retry.
|
|
89
|
+
|
|
90
|
+
## The domain-first model
|
|
91
|
+
|
|
92
|
+
MailBlastr is DOMAIN-FIRST: each of your verified sending domains has its own
|
|
93
|
+
contact pool — the same address on two domains is two records with separate
|
|
94
|
+
consent. That means:
|
|
95
|
+
|
|
96
|
+
- `Contacts` take a `domain` (required to create/list on the flat `/contacts`
|
|
97
|
+
API; disambiguates an email id on get/update/remove).
|
|
98
|
+
- `Segments` and `Topics` belong to a domain (`domain` required on create and list).
|
|
99
|
+
- `Campaigns.create` REQUIRES `domain` — it picks the contact pool the campaign
|
|
100
|
+
targets (the `from` address may be a different verified domain).
|
|
101
|
+
- `Automations.create` REQUIRES `domain`, and `Events.send` REQUIRES `domain` —
|
|
102
|
+
only automations belonging to that domain are triggered, so the same event
|
|
103
|
+
name (e.g. `user.created`) across several products can never double-fire.
|
|
104
|
+
|
|
105
|
+
## Resources
|
|
106
|
+
|
|
107
|
+
Each resource is a class with methods following a consistent
|
|
108
|
+
`create` / `get` / `list` / `update` / `remove` shape (plus resource-specific verbs):
|
|
109
|
+
`Emails` (with nested `Emails.Attachments` and `Emails.Receiving`), `Batch`,
|
|
110
|
+
`Domains`, `Audiences`, `Contacts`, `ContactProperties`, `Campaigns`,
|
|
111
|
+
`Segments`, `Topics`, `Templates`, `Automations`, `Webhooks`, `Events`,
|
|
112
|
+
`ApiKeys`, `Logs`, `Polls`.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Emails
|
|
116
|
+
mailblastr.Emails.send(params)
|
|
117
|
+
mailblastr.Emails.list({"limit": 20, "after": cursor}) # cursor pagination
|
|
118
|
+
mailblastr.Emails.get(email_id)
|
|
119
|
+
mailblastr.Emails.update(email_id, {"scheduled_at": "2026-08-01T09:00:00Z"}) # reschedule
|
|
120
|
+
mailblastr.Emails.cancel(email_id)
|
|
121
|
+
mailblastr.Emails.Attachments.list(email_id)
|
|
122
|
+
mailblastr.Emails.Attachments.get(email_id, attachment_id)
|
|
123
|
+
|
|
124
|
+
# Inbound email
|
|
125
|
+
mailblastr.Emails.Receiving.list()
|
|
126
|
+
mailblastr.Emails.Receiving.get(email_id)
|
|
127
|
+
mailblastr.Emails.Receiving.attachments(email_id)
|
|
128
|
+
mailblastr.Emails.Receiving.get_attachment(email_id, attachment_id) # -> bytes
|
|
129
|
+
mailblastr.Emails.Receiving.raw(email_id) # -> bytes (RFC822)
|
|
130
|
+
mailblastr.Emails.Receiving.forward(email_id, {"from": "me@yourdomain.com", "to": "team@you.com"})
|
|
131
|
+
mailblastr.Emails.Receiving.reply(email_id, {"from": "me@yourdomain.com", "html": "<p>Thanks!</p>"})
|
|
132
|
+
mailblastr.Emails.Receiving.remove(email_id)
|
|
133
|
+
|
|
134
|
+
# Domains (incl. claiming a domain verified elsewhere + one-click DNS)
|
|
135
|
+
mailblastr.Domains.create({"name": "yourdomain.com"})
|
|
136
|
+
mailblastr.Domains.verify(domain_id)
|
|
137
|
+
mailblastr.Domains.claim({"name": "yourdomain.com"})
|
|
138
|
+
mailblastr.Domains.verify_claim(domain_id)
|
|
139
|
+
mailblastr.Domains.detect_dns(domain_id)
|
|
140
|
+
mailblastr.Domains.apply_cloudflare_dns(domain_id, {"token": cf_token})
|
|
141
|
+
|
|
142
|
+
# Contacts (domain-first)
|
|
143
|
+
mailblastr.Contacts.create({"domain": "yourdomain.com", "email": "user@example.com", "first_name": "Ada"})
|
|
144
|
+
mailblastr.Contacts.list({"domain": "yourdomain.com"})
|
|
145
|
+
mailblastr.Contacts.get({"id": contact_id}) # by id (exact) …
|
|
146
|
+
mailblastr.Contacts.get({"id": "user@example.com", "domain": "yourdomain.com"}) # … or by email + domain
|
|
147
|
+
mailblastr.Contacts.update({"id": contact_id, "unsubscribed": True})
|
|
148
|
+
mailblastr.Contacts.remove({"id": contact_id})
|
|
149
|
+
mailblastr.Contacts.batch({"audience_id": aud_id, "contacts": [{"email": "a@b.com"}]})
|
|
150
|
+
mailblastr.Contacts.import_csv({"audience_id": aud_id, "csv": "email,company\na@b.com,Acme"})
|
|
151
|
+
mailblastr.Contacts.add_to_segment(contact_id, segment_id)
|
|
152
|
+
mailblastr.Contacts.list_segments(contact_id)
|
|
153
|
+
mailblastr.Contacts.update_topics(contact_id, {"topics": [{"id": topic_id, "subscription": "opt_in"}]})
|
|
154
|
+
|
|
155
|
+
# Contact properties (custom fields / merge tags)
|
|
156
|
+
mailblastr.ContactProperties.create({"key": "plan", "type": "string"})
|
|
157
|
+
|
|
158
|
+
# Campaigns & Segments (domain-first)
|
|
159
|
+
mailblastr.Campaigns.create({"domain": "yourdomain.com", "from": sender, "subject": subject, "html": html})
|
|
160
|
+
mailblastr.Campaigns.send(campaign_id, {"scheduled_at": "tomorrow 9am"})
|
|
161
|
+
mailblastr.Campaigns.stats(campaign_id)
|
|
162
|
+
mailblastr.Campaigns.ab(campaign_id)
|
|
163
|
+
mailblastr.Segments.create({"domain": "yourdomain.com", "name": "VIP", "filter": {"status": "subscribed"}})
|
|
164
|
+
mailblastr.Segments.list({"domain": "yourdomain.com"})
|
|
165
|
+
mailblastr.Segments.contacts(segment_id) # preview who matches
|
|
166
|
+
|
|
167
|
+
# Topics (domain-first)
|
|
168
|
+
mailblastr.Topics.create({"domain": "yourdomain.com", "name": "Product updates", "default_subscription": "opt_in"})
|
|
169
|
+
mailblastr.Topics.list({"domain": "yourdomain.com"})
|
|
170
|
+
|
|
171
|
+
# Templates
|
|
172
|
+
mailblastr.Templates.create({"name": "Welcome", "subject": "Hi {{first_name}}", "html": html})
|
|
173
|
+
mailblastr.Templates.duplicate(template_id)
|
|
174
|
+
mailblastr.Templates.publish(template_id)
|
|
175
|
+
mailblastr.Emails.send({"from": sender, "to": to, "template_id": tmpl_id, "variables": {"first_name": "Ada"}})
|
|
176
|
+
|
|
177
|
+
# Audiences
|
|
178
|
+
mailblastr.Audiences.list()
|
|
179
|
+
mailblastr.Audiences.import_sheet(audience_id, {"url": sheet_url})
|
|
180
|
+
|
|
181
|
+
# API keys
|
|
182
|
+
mailblastr.ApiKeys.create({"name": "CI", "permission": "sending_access"})
|
|
183
|
+
mailblastr.ApiKeys.list()
|
|
184
|
+
mailblastr.ApiKeys.remove(key_id)
|
|
185
|
+
|
|
186
|
+
# Logs & Polls
|
|
187
|
+
mailblastr.Logs.list({"limit": 100, "method": "POST", "status": 429})
|
|
188
|
+
mailblastr.Logs.get(log_id)
|
|
189
|
+
mailblastr.Polls.list()
|
|
190
|
+
mailblastr.Polls.get(email_id)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Automations & Events
|
|
194
|
+
|
|
195
|
+
Every automation belongs to one of your sending domains — `domain` is required
|
|
196
|
+
on create, and `Events.send` names the domain it targets.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
automation = mailblastr.Automations.create({
|
|
200
|
+
"name": "Welcome series",
|
|
201
|
+
"domain": "yourdomain.com",
|
|
202
|
+
"trigger": "contact.created",
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
mailblastr.Automations.add_step(automation["id"], {
|
|
206
|
+
"type": "send_email",
|
|
207
|
+
"config": {"template_id": "tmpl_welcome"},
|
|
208
|
+
})
|
|
209
|
+
mailblastr.Automations.update(automation["id"], {"status": "enabled"})
|
|
210
|
+
|
|
211
|
+
# Fire a custom event — only yourdomain.com's automations are triggered
|
|
212
|
+
mailblastr.Events.send({
|
|
213
|
+
"event": "signup.completed",
|
|
214
|
+
"domain": "yourdomain.com",
|
|
215
|
+
"email": "user@example.com",
|
|
216
|
+
"payload": {"plan": "pro"},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
# Inspect execution
|
|
220
|
+
runs = mailblastr.Automations.runs(automation["id"], {"limit": 25})
|
|
221
|
+
mailblastr.Automations.get_run(automation["id"], runs["data"][0]["id"])
|
|
222
|
+
mailblastr.Automations.stop(automation["id"])
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Webhooks
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
hook = mailblastr.Webhooks.create({
|
|
229
|
+
"endpoint": "https://yourapp.com/hooks/mailblastr",
|
|
230
|
+
"events": ["email.delivered", "email.bounced", "contact.unsubscribed"],
|
|
231
|
+
})
|
|
232
|
+
signing_secret = hook["signing_secret"] # shown ONCE, only here
|
|
233
|
+
|
|
234
|
+
mailblastr.Webhooks.list()
|
|
235
|
+
mailblastr.Webhooks.update(hook["id"], {"status": "disabled"})
|
|
236
|
+
mailblastr.Webhooks.rotate(hook["id"]) # new secret, returned once
|
|
237
|
+
mailblastr.Webhooks.test(hook["id"])
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Verify incoming deliveries locally (no HTTP request) — pass the EXACT raw
|
|
241
|
+
request body string, the `svix-*` headers, and your signing secret:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
result = mailblastr.Webhooks.verify(raw_body, request.headers, signing_secret)
|
|
245
|
+
if not result["valid"]:
|
|
246
|
+
abort(401) # result["reason"] says why, e.g. 'no_match'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Pagination
|
|
250
|
+
|
|
251
|
+
`list()` methods accept optional cursor pagination — `{"limit", "after", "before"}`:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
mailblastr.Campaigns.list({"limit": 25, "after": "cursor_abc"})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Idempotency
|
|
258
|
+
|
|
259
|
+
Pass an idempotency key to safely retry a create (24h window):
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
mailblastr.Emails.send(params, options={"idempotency_key": "order-123"})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Documentation
|
|
266
|
+
|
|
267
|
+
Full docs: <https://www.mailblastr.com/docs>
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Official MailBlastr Python SDK.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import mailblastr
|
|
6
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
7
|
+
|
|
8
|
+
email = mailblastr.Emails.send({
|
|
9
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
10
|
+
"to": ["user@example.com"],
|
|
11
|
+
"subject": "Hello from MailBlastr",
|
|
12
|
+
"html": "<p>Your first email</p>",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
Every method returns the parsed JSON response and raises
|
|
16
|
+
:class:`mailblastr.MailblastrError` on any non-2xx status.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from .http_client import DEFAULT_BASE_URL, USER_AGENT, VERSION
|
|
22
|
+
from .exceptions import MailblastrError
|
|
23
|
+
from .emails import Emails
|
|
24
|
+
from .batch import Batch
|
|
25
|
+
from .domains import Domains
|
|
26
|
+
from .audiences import Audiences
|
|
27
|
+
from .contacts import Contacts
|
|
28
|
+
from .contact_properties import ContactProperties
|
|
29
|
+
from .campaigns import Campaigns
|
|
30
|
+
from .segments import Segments
|
|
31
|
+
from .topics import Topics
|
|
32
|
+
from .templates import Templates
|
|
33
|
+
from .automations import Automations
|
|
34
|
+
from .webhooks import Webhooks, verify_webhook_signature
|
|
35
|
+
from .events import Events
|
|
36
|
+
from .api_keys import ApiKeys
|
|
37
|
+
from .logs import Logs
|
|
38
|
+
from .polls import Polls
|
|
39
|
+
|
|
40
|
+
# ---- Module-level configuration (resend-python style) ----
|
|
41
|
+
|
|
42
|
+
# Your MailBlastr API key, e.g. mailblastr.api_key = "mb_xxxxxxxxx"
|
|
43
|
+
api_key: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
# Override to point at a different API host.
|
|
46
|
+
base_url: str = DEFAULT_BASE_URL
|
|
47
|
+
|
|
48
|
+
__version__ = VERSION
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"api_key",
|
|
52
|
+
"base_url",
|
|
53
|
+
"MailblastrError",
|
|
54
|
+
"verify_webhook_signature",
|
|
55
|
+
"Emails",
|
|
56
|
+
"Batch",
|
|
57
|
+
"Domains",
|
|
58
|
+
"Audiences",
|
|
59
|
+
"Contacts",
|
|
60
|
+
"ContactProperties",
|
|
61
|
+
"Campaigns",
|
|
62
|
+
"Segments",
|
|
63
|
+
"Topics",
|
|
64
|
+
"Templates",
|
|
65
|
+
"Automations",
|
|
66
|
+
"Webhooks",
|
|
67
|
+
"Events",
|
|
68
|
+
"ApiKeys",
|
|
69
|
+
"Logs",
|
|
70
|
+
"Polls",
|
|
71
|
+
"DEFAULT_BASE_URL",
|
|
72
|
+
"USER_AGENT",
|
|
73
|
+
"VERSION",
|
|
74
|
+
]
|