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.
Files changed (36) hide show
  1. mailblastr-1.0.0/LICENSE +21 -0
  2. mailblastr-1.0.0/PKG-INFO +298 -0
  3. mailblastr-1.0.0/README.md +271 -0
  4. mailblastr-1.0.0/mailblastr/__init__.py +74 -0
  5. mailblastr-1.0.0/mailblastr/api_keys.py +34 -0
  6. mailblastr-1.0.0/mailblastr/audiences.py +56 -0
  7. mailblastr-1.0.0/mailblastr/automations.py +98 -0
  8. mailblastr-1.0.0/mailblastr/batch.py +14 -0
  9. mailblastr-1.0.0/mailblastr/campaigns.py +110 -0
  10. mailblastr-1.0.0/mailblastr/contact_properties.py +50 -0
  11. mailblastr-1.0.0/mailblastr/contacts.py +191 -0
  12. mailblastr-1.0.0/mailblastr/domains.py +109 -0
  13. mailblastr-1.0.0/mailblastr/emails.py +180 -0
  14. mailblastr-1.0.0/mailblastr/events.py +52 -0
  15. mailblastr-1.0.0/mailblastr/exceptions.py +25 -0
  16. mailblastr-1.0.0/mailblastr/http_client.py +173 -0
  17. mailblastr-1.0.0/mailblastr/logs.py +41 -0
  18. mailblastr-1.0.0/mailblastr/polls.py +18 -0
  19. mailblastr-1.0.0/mailblastr/segments.py +76 -0
  20. mailblastr-1.0.0/mailblastr/templates.py +89 -0
  21. mailblastr-1.0.0/mailblastr/topics.py +71 -0
  22. mailblastr-1.0.0/mailblastr/webhooks.py +160 -0
  23. mailblastr-1.0.0/mailblastr.egg-info/PKG-INFO +298 -0
  24. mailblastr-1.0.0/mailblastr.egg-info/SOURCES.txt +34 -0
  25. mailblastr-1.0.0/mailblastr.egg-info/dependency_links.txt +1 -0
  26. mailblastr-1.0.0/mailblastr.egg-info/top_level.txt +1 -0
  27. mailblastr-1.0.0/pyproject.toml +35 -0
  28. mailblastr-1.0.0/setup.cfg +4 -0
  29. mailblastr-1.0.0/tests/test_campaigns.py +54 -0
  30. mailblastr-1.0.0/tests/test_contacts.py +108 -0
  31. mailblastr-1.0.0/tests/test_domains.py +48 -0
  32. mailblastr-1.0.0/tests/test_emails.py +111 -0
  33. mailblastr-1.0.0/tests/test_http_client.py +220 -0
  34. mailblastr-1.0.0/tests/test_misc_resources.py +150 -0
  35. mailblastr-1.0.0/tests/test_segments.py +53 -0
  36. mailblastr-1.0.0/tests/test_webhooks.py +119 -0
@@ -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
+ ]