euromail 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.
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Build & Test
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install -e ".[dev]"
18
+ - run: python -m pytest --no-header -q || true
19
+ - run: pip install build && python -m build
20
+
21
+ publish:
22
+ name: Publish to PyPI
23
+ needs: test
24
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ permissions:
28
+ id-token: write
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+ - run: pip install build && python -m build
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ .venv/
5
+ *.pyc
@@ -0,0 +1,472 @@
1
+ Metadata-Version: 2.4
2
+ Name: euromail
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the EuroMail transactional email service
5
+ License-Expression: MIT
6
+ Keywords: email,euromail,smtp,transactional
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: httpx>=0.27
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Requires-Dist: respx>=0.22; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # euromail
16
+
17
+ Official Python SDK for the [EuroMail](https://euromail.dev) transactional email service.
18
+
19
+ [![PyPI](https://img.shields.io/pypi/v/euromail.svg)](https://pypi.org/project/euromail/)
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install euromail
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```python
30
+ from euromail import EuroMail
31
+
32
+ client = EuroMail(api_key="em_live_your_api_key_here")
33
+
34
+ result = client.send_email(
35
+ from_address="sender@yourdomain.com",
36
+ to="recipient@example.com",
37
+ subject="Hello from EuroMail",
38
+ html_body="<h1>Welcome!</h1><p>Your account is ready.</p>",
39
+ )
40
+ print(f"Email queued: {result.id}")
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ```python
46
+ client = EuroMail(
47
+ api_key="em_live_...", # Required
48
+ timeout=30.0, # Default: 30 seconds
49
+ )
50
+ ```
51
+
52
+ ### Context manager
53
+
54
+ The client can be used as a context manager to ensure the HTTP connection is properly closed:
55
+
56
+ ```python
57
+ with EuroMail(api_key="em_live_...") as client:
58
+ client.send_email(
59
+ from_address="noreply@yourdomain.com",
60
+ to="user@example.com",
61
+ subject="Hello",
62
+ text_body="Welcome aboard!",
63
+ )
64
+ ```
65
+
66
+ ### Async client
67
+
68
+ For async/await applications, use `AsyncEuroMail`:
69
+
70
+ ```python
71
+ from euromail import AsyncEuroMail
72
+
73
+ async with AsyncEuroMail(api_key="em_live_...") as client:
74
+ result = await client.send_email(
75
+ from_address="noreply@yourdomain.com",
76
+ to="user@example.com",
77
+ subject="Hello",
78
+ text_body="Welcome aboard!",
79
+ )
80
+ ```
81
+
82
+ The async client has the same API as the sync client, with all methods being `async`.
83
+
84
+ ## Sending Emails
85
+
86
+ ### Direct send
87
+
88
+ ```python
89
+ result = client.send_email(
90
+ from_address="noreply@yourdomain.com",
91
+ to="user@example.com",
92
+ subject="Order Confirmation",
93
+ html_body="<h1>Thanks for your order!</h1>",
94
+ text_body="Thanks for your order!",
95
+ reply_to="support@yourdomain.com",
96
+ cc=["manager@yourdomain.com"],
97
+ tags=["order", "confirmation"],
98
+ metadata={"order_id": "12345"},
99
+ )
100
+ ```
101
+
102
+ ### Send with template
103
+
104
+ ```python
105
+ result = client.send_email(
106
+ from_address="noreply@yourdomain.com",
107
+ to="user@example.com",
108
+ template_alias="welcome-email",
109
+ template_data={
110
+ "name": "John",
111
+ "activation_url": "https://example.com/activate/abc123",
112
+ },
113
+ )
114
+ ```
115
+
116
+ ### Batch send
117
+
118
+ ```python
119
+ from euromail import SendEmailParams
120
+
121
+ batch = client.send_batch(
122
+ emails=[
123
+ SendEmailParams(
124
+ from_address="noreply@yourdomain.com",
125
+ to="user1@example.com",
126
+ subject="Hello User 1",
127
+ text_body="Welcome aboard!",
128
+ ),
129
+ SendEmailParams(
130
+ from_address="noreply@yourdomain.com",
131
+ to="user2@example.com",
132
+ subject="Hello User 2",
133
+ text_body="Welcome aboard!",
134
+ ),
135
+ ]
136
+ )
137
+ print(f"Sent: {len(batch.data)}, Errors: {len(batch.errors)}")
138
+ ```
139
+
140
+ ### Idempotent sends
141
+
142
+ ```python
143
+ client.send_email(
144
+ from_address="noreply@yourdomain.com",
145
+ to="user@example.com",
146
+ subject="Payment Receipt",
147
+ html_body="<p>Payment received.</p>",
148
+ idempotency_key="payment-receipt-12345",
149
+ )
150
+ ```
151
+
152
+ ### Retrieve and list emails
153
+
154
+ ```python
155
+ email = client.get_email("email-uuid")
156
+
157
+ emails = client.list_emails(page=1, per_page=50, status="delivered")
158
+ for e in emails.data:
159
+ print(f"{e.to_address}: {e.status}")
160
+ ```
161
+
162
+ ## Domains
163
+
164
+ ```python
165
+ # Add a sending domain
166
+ domain = client.add_domain("mail.yourdomain.com")
167
+ print("Configure DNS records:", domain.dns_records)
168
+
169
+ # Trigger verification
170
+ verification = client.verify_domain(domain.id)
171
+ if verification.fully_verified:
172
+ print("Domain verified! SPF, DKIM, DMARC, and return-path all confirmed.")
173
+
174
+ # List all domains
175
+ domains = client.list_domains(page=1, per_page=25)
176
+
177
+ # Remove a domain
178
+ client.delete_domain(domain.id)
179
+ ```
180
+
181
+ ## Templates
182
+
183
+ ```python
184
+ # Create a template with Jinja2-style variables
185
+ template = client.create_template(
186
+ alias="welcome-email",
187
+ name="Welcome Email",
188
+ subject="Welcome, {{ name }}!",
189
+ html_body="<h1>Hello {{ name }}</h1><p>Welcome to {{ company }}.</p>",
190
+ text_body="Hello {{ name }}, welcome to {{ company }}.",
191
+ )
192
+
193
+ # Update a template
194
+ client.update_template(template.id, subject="Welcome to {{ company }}, {{ name }}!")
195
+
196
+ # List and delete
197
+ templates = client.list_templates(page=1, per_page=25)
198
+ client.delete_template(template.id)
199
+ ```
200
+
201
+ ## Webhooks
202
+
203
+ ```python
204
+ # Subscribe to delivery events
205
+ webhook = client.create_webhook(
206
+ url="https://yourdomain.com/webhooks/euromail",
207
+ events=["delivered", "bounced", "complained", "email.inbound"],
208
+ )
209
+
210
+ # Update webhook
211
+ client.update_webhook(
212
+ webhook.id,
213
+ url="https://yourdomain.com/webhooks/v2",
214
+ events=["delivered", "bounced"],
215
+ is_active=True,
216
+ )
217
+
218
+ # Send a test event
219
+ test = client.test_webhook(webhook.id)
220
+
221
+ # List and delete
222
+ webhooks = client.list_webhooks()
223
+ client.delete_webhook(webhook.id)
224
+ ```
225
+
226
+ Supported events: `sent`, `delivered`, `bounced`, `opened`, `clicked`, `complained`, `email.inbound`
227
+
228
+ ## Suppressions
229
+
230
+ ```python
231
+ # Suppress an address manually
232
+ client.add_suppression("bounced@example.com", reason="hard_bounce")
233
+
234
+ # List all suppressions
235
+ suppressions = client.list_suppressions(page=1, per_page=50)
236
+
237
+ # Remove a suppression
238
+ client.delete_suppression("bounced@example.com")
239
+ ```
240
+
241
+ ## Contact Lists
242
+
243
+ ```python
244
+ # Create a list with double opt-in
245
+ contact_list = client.create_contact_list(
246
+ name="Newsletter",
247
+ description="Monthly product updates",
248
+ double_opt_in=True,
249
+ )
250
+
251
+ # Add a single contact
252
+ contact = client.add_contact(
253
+ contact_list.id,
254
+ email="user@example.com",
255
+ metadata={"first_name": "Jane", "source": "signup"},
256
+ )
257
+
258
+ # Bulk add contacts
259
+ result = client.bulk_add_contacts(
260
+ contact_list.id,
261
+ contacts=[
262
+ {"email": "a@example.com", "metadata": {"name": "Alice"}},
263
+ {"email": "b@example.com", "metadata": {"name": "Bob"}},
264
+ ],
265
+ )
266
+ print(f"Inserted: {result.inserted} of {result.total_requested}")
267
+
268
+ # List contacts with filters
269
+ contacts = client.list_contacts(contact_list.id, page=1, per_page=50, status="active")
270
+
271
+ # Remove contact and delete list
272
+ client.remove_contact(contact_list.id, "user@example.com")
273
+ client.delete_contact_list(contact_list.id)
274
+ ```
275
+
276
+ ## Inbound Email
277
+
278
+ ```python
279
+ # List received emails
280
+ inbound = client.list_inbound_emails(page=1, per_page=25)
281
+
282
+ # Get details
283
+ email = client.get_inbound_email("inbound-uuid")
284
+ print(f"From: {email.from_address}, Subject: {email.subject}")
285
+
286
+ # Delete
287
+ client.delete_inbound_email("inbound-uuid")
288
+ ```
289
+
290
+ ## Inbound Routes
291
+
292
+ ```python
293
+ # Route incoming email to a webhook
294
+ route = client.create_inbound_route(
295
+ domain_id="domain-uuid",
296
+ pattern="support@",
297
+ match_type="prefix",
298
+ priority=10,
299
+ webhook_url="https://yourdomain.com/inbound/support",
300
+ )
301
+
302
+ # Update a route
303
+ client.update_inbound_route(route.id, webhook_url="https://yourdomain.com/inbound/v2", is_active=True)
304
+
305
+ # Catch-all route
306
+ client.create_inbound_route(
307
+ domain_id="domain-uuid",
308
+ pattern="*",
309
+ match_type="catch_all",
310
+ priority=100,
311
+ )
312
+
313
+ # List and delete
314
+ routes = client.list_inbound_routes()
315
+ client.delete_inbound_route(route.id)
316
+ ```
317
+
318
+ ## Analytics
319
+
320
+ ```python
321
+ # Overview for the last 30 days
322
+ overview = client.get_analytics_overview(period="30d")
323
+ print(f"Delivery rate: {overview.delivery_rate}%")
324
+
325
+ # Custom date range
326
+ custom = client.get_analytics_overview(from_date="2025-01-01", to_date="2025-01-31")
327
+
328
+ # Time series data
329
+ timeseries = client.get_analytics_timeseries(period="7d", metrics=["sent", "delivered", "bounced"])
330
+
331
+ # Per-domain breakdown
332
+ domains = client.get_analytics_domains(period="30d", limit=10)
333
+
334
+ # Export as CSV
335
+ csv_data = client.export_analytics_csv(period="30d")
336
+ ```
337
+
338
+ ## Account
339
+
340
+ ```python
341
+ account = client.get_account()
342
+ print(f"Plan: {account.plan}, Used: {account.emails_sent_this_month}/{account.monthly_quota}")
343
+
344
+ # Export all account data (GDPR)
345
+ export_data = client.export_account()
346
+
347
+ # Delete account permanently
348
+ client.delete_account()
349
+ ```
350
+
351
+ ## Audit Logs
352
+
353
+ ```python
354
+ logs = client.list_audit_logs(page=1, per_page=50)
355
+ for log in logs.data:
356
+ print(f"{log.created_at}: {log.action} on {log.resource_type}")
357
+ ```
358
+
359
+ ## Dead Letters
360
+
361
+ ```python
362
+ # List failed emails
363
+ dead_letters = client.list_dead_letters(count=20)
364
+
365
+ # Retry delivery
366
+ client.retry_dead_letter("dead-letter-uuid")
367
+
368
+ # Remove permanently
369
+ client.delete_dead_letter("dead-letter-uuid")
370
+ ```
371
+
372
+ ## Error Handling
373
+
374
+ All API errors raise typed exceptions:
375
+
376
+ ```python
377
+ from euromail import (
378
+ EuroMail,
379
+ EuroMailError,
380
+ AuthenticationError,
381
+ RateLimitError,
382
+ ValidationError,
383
+ )
384
+
385
+ try:
386
+ client.send_email(...)
387
+ except AuthenticationError:
388
+ # Invalid or missing API key (401)
389
+ print("Check your API key")
390
+ except ValidationError as e:
391
+ # Invalid request parameters (422)
392
+ print(f"{e.code}: {e.message}")
393
+ except RateLimitError as e:
394
+ # Too many requests (429)
395
+ print(f"Retry after {e.retry_after} seconds")
396
+ except EuroMailError as e:
397
+ # Other API errors (4xx/5xx)
398
+ print(f"[{e.status}] {e.code}: {e.message}")
399
+ ```
400
+
401
+ | Exception | HTTP Status | Description |
402
+ |---|---|---|
403
+ | `AuthenticationError` | 401 | Invalid or missing API key |
404
+ | `ValidationError` | 422 | Invalid request parameters |
405
+ | `RateLimitError` | 429 | Too many requests (includes `retry_after`) |
406
+ | `EuroMailError` | 4xx/5xx | Base class for all API errors |
407
+
408
+ ## API Reference
409
+
410
+ | Category | Method | Description |
411
+ |---|---|---|
412
+ | **Emails** | `send_email(...)` | Send a single email |
413
+ | | `send_batch(emails=...)` | Send up to 500 emails in one request |
414
+ | | `get_email(id)` | Get email details and status |
415
+ | | `list_emails(...)` | List emails with pagination and status filter |
416
+ | **Templates** | `create_template(...)` | Create an email template |
417
+ | | `get_template(id)` | Get template by ID |
418
+ | | `update_template(id, ...)` | Update template fields |
419
+ | | `delete_template(id)` | Delete a template |
420
+ | | `list_templates(...)` | List templates with pagination |
421
+ | **Domains** | `add_domain(domain)` | Register a sending domain |
422
+ | | `get_domain(id)` | Get domain details and DNS records |
423
+ | | `verify_domain(id)` | Trigger DNS verification |
424
+ | | `delete_domain(id)` | Remove a domain |
425
+ | | `list_domains(...)` | List domains with pagination |
426
+ | **Webhooks** | `create_webhook(...)` | Subscribe to events |
427
+ | | `get_webhook(id)` | Get webhook details |
428
+ | | `update_webhook(id, ...)` | Update URL, events, or status |
429
+ | | `test_webhook(id)` | Send a test event |
430
+ | | `delete_webhook(id)` | Remove a webhook |
431
+ | | `list_webhooks(...)` | List webhooks with pagination |
432
+ | **Suppressions** | `add_suppression(email, ...)` | Suppress an email address |
433
+ | | `delete_suppression(email)` | Remove a suppression |
434
+ | | `list_suppressions(...)` | List suppressions with pagination |
435
+ | **Contact Lists** | `create_contact_list(...)` | Create a contact list |
436
+ | | `get_contact_list(id)` | Get list details |
437
+ | | `update_contact_list(id, ...)` | Update list settings |
438
+ | | `delete_contact_list(id)` | Delete a list |
439
+ | | `list_contact_lists()` | List all contact lists |
440
+ | | `add_contact(list_id, ...)` | Add a contact to a list |
441
+ | | `bulk_add_contacts(list_id, ...)` | Add multiple contacts |
442
+ | | `list_contacts(list_id, ...)` | List contacts with filters |
443
+ | | `remove_contact(list_id, email)` | Remove a contact |
444
+ | **Inbound** | `list_inbound_emails(...)` | List received emails |
445
+ | | `get_inbound_email(id)` | Get inbound email details |
446
+ | | `delete_inbound_email(id)` | Delete an inbound email |
447
+ | **Inbound Routes** | `create_inbound_route(...)` | Create a routing rule |
448
+ | | `get_inbound_route(id)` | Get route details |
449
+ | | `update_inbound_route(id, ...)` | Update a route |
450
+ | | `delete_inbound_route(id)` | Delete a route |
451
+ | | `list_inbound_routes(...)` | List routes with pagination |
452
+ | **Analytics** | `get_analytics_overview(...)` | Aggregated delivery stats |
453
+ | | `get_analytics_timeseries(...)` | Daily metrics over time |
454
+ | | `get_analytics_domains(...)` | Per-domain breakdown |
455
+ | | `export_analytics_csv(...)` | Export stats as CSV |
456
+ | **Audit Logs** | `list_audit_logs(...)` | List account activity |
457
+ | **Dead Letters** | `list_dead_letters(...)` | List permanently failed emails |
458
+ | | `retry_dead_letter(id)` | Retry delivery |
459
+ | | `delete_dead_letter(id)` | Remove from dead letter queue |
460
+ | **Account** | `get_account()` | Get account info and quota |
461
+ | | `export_account()` | Export all account data |
462
+ | | `delete_account()` | Permanently delete account |
463
+
464
+ ## Requirements
465
+
466
+ - Python 3.9+
467
+ - httpx >= 0.27
468
+
469
+ ## License
470
+
471
+ MIT
472
+