euromail
Official Python SDK for the EuroMail transactional email service.
Installation
pip install euromail
Quick Start
from euromail import EuroMail
client = EuroMail(api_key="em_live_your_api_key_here")
result = client.send_email(
from_address="sender@yourdomain.com",
to="recipient@example.com",
subject="Hello from EuroMail",
html_body="<h1>Welcome!</h1><p>Your account is ready.</p>",
)
print(f"Email queued: {result.id}")
Configuration
client = EuroMail(
api_key="em_live_...", # Required
timeout=30.0, # Default: 30 seconds
)
Context manager
The client can be used as a context manager to ensure the HTTP connection is properly closed:
with EuroMail(api_key="em_live_...") as client:
client.send_email(
from_address="noreply@yourdomain.com",
to="user@example.com",
subject="Hello",
text_body="Welcome aboard!",
)
Async client
For async/await applications, use AsyncEuroMail:
from euromail import AsyncEuroMail
async with AsyncEuroMail(api_key="em_live_...") as client:
result = await client.send_email(
from_address="noreply@yourdomain.com",
to="user@example.com",
subject="Hello",
text_body="Welcome aboard!",
)
The async client has the same API as the sync client, with all methods being async.
Sending Emails
Direct send
result = client.send_email(
from_address="noreply@yourdomain.com",
to="user@example.com",
subject="Order Confirmation",
html_body="<h1>Thanks for your order!</h1>",
text_body="Thanks for your order!",
reply_to="support@yourdomain.com",
cc=["manager@yourdomain.com"],
tags=["order", "confirmation"],
metadata={"order_id": "12345"},
)
Send with template
result = client.send_email(
from_address="noreply@yourdomain.com",
to="user@example.com",
template_alias="welcome-email",
template_data={
"name": "John",
"activation_url": "https://example.com/activate/abc123",
},
)
Batch send
from euromail import SendEmailParams
batch = client.send_batch(
emails=[
SendEmailParams(
from_address="noreply@yourdomain.com",
to="user1@example.com",
subject="Hello User 1",
text_body="Welcome aboard!",
),
SendEmailParams(
from_address="noreply@yourdomain.com",
to="user2@example.com",
subject="Hello User 2",
text_body="Welcome aboard!",
),
]
)
print(f"Sent: {len(batch.data)}, Errors: {len(batch.errors)}")
Idempotent sends
client.send_email(
from_address="noreply@yourdomain.com",
to="user@example.com",
subject="Payment Receipt",
html_body="<p>Payment received.</p>",
idempotency_key="payment-receipt-12345",
)
Scheduling, tracking, and marketing sends
client.send_email(
from_address="news@yourdomain.com",
to="user@example.com",
subject="This week in your inbox",
html_body="<p>Latest updates...</p>",
send_at="2026-08-01T09:00:00Z", # schedule delivery
tracking=True, # per-email open/click override
transactional=False, # adds List-Unsubscribe for marketing/newsletter mail
stream="marketing", # isolate reputation from transactional sends
)
Retrieve and list emails
email = client.get_email("email-uuid")
emails = client.list_emails(page=1, per_page=50, status="delivered")
for e in emails.data:
print(f"{e.to_address}: {e.status}")
Domains
# Add a sending domain
domain = client.add_domain("mail.yourdomain.com")
print("Configure DNS records:", domain.dns_records)
# Trigger verification
verification = client.verify_domain(domain.id)
if verification.fully_verified:
print("Domain verified! SPF, DKIM, DMARC, and return-path all confirmed.")
# List all domains
domains = client.list_domains(page=1, per_page=25)
# Remove a domain
client.delete_domain(domain.id)
Templates
# Create a template with Jinja2-style variables
template = client.create_template(
alias="welcome-email",
name="Welcome Email",
subject="Welcome, {{ name }}!",
html_body="<h1>Hello {{ name }}</h1><p>Welcome to {{ company }}.</p>",
text_body="Hello {{ name }}, welcome to {{ company }}.",
)
# Update a template
client.update_template(template.id, subject="Welcome to {{ company }}, {{ name }}!")
# List and delete
templates = client.list_templates(page=1, per_page=25)
client.delete_template(template.id)
Webhooks
# Subscribe to delivery events
webhook = client.create_webhook(
url="https://yourdomain.com/webhooks/euromail",
events=["delivered", "bounced", "complained", "email.inbound"],
)
# Update webhook
client.update_webhook(
webhook.id,
url="https://yourdomain.com/webhooks/v2",
events=["delivered", "bounced"],
is_active=True,
)
# Send a test event
test = client.test_webhook(webhook.id)
# List and delete
webhooks = client.list_webhooks()
client.delete_webhook(webhook.id)
Supported events: sent, delivered, bounced, opened, clicked, complained, email.inbound
Suppressions
# Suppress an address manually
client.add_suppression("bounced@example.com", reason="hard_bounce")
# List all suppressions
suppressions = client.list_suppressions(page=1, per_page=50)
# Remove a suppression
client.delete_suppression("bounced@example.com")
Contact Lists
# Create a list with double opt-in
contact_list = client.create_contact_list(
name="Newsletter",
description="Monthly product updates",
double_opt_in=True,
)
# Add a single contact
contact = client.add_contact(
contact_list.id,
email="user@example.com",
metadata={"first_name": "Jane", "source": "signup"},
)
# Bulk add contacts
result = client.bulk_add_contacts(
contact_list.id,
contacts=[
{"email": "a@example.com", "metadata": {"name": "Alice"}},
{"email": "b@example.com", "metadata": {"name": "Bob"}},
],
)
print(f"Inserted: {result.inserted} of {result.total_requested}")
# List contacts with filters
contacts = client.list_contacts(contact_list.id, page=1, per_page=50, status="active")
# Remove contact and delete list
client.remove_contact(contact_list.id, "user@example.com")
client.delete_contact_list(contact_list.id)
Inbound Email
# List received emails
inbound = client.list_inbound_emails(page=1, per_page=25)
# Get details
email = client.get_inbound_email("inbound-uuid")
print(f"From: {email.mail_from}, Subject: {email.subject}")
# Delete
client.delete_inbound_email("inbound-uuid")
Inbound Routes
# Route incoming email to a webhook
route = client.create_inbound_route(
domain_id="domain-uuid",
pattern="support@",
match_type="prefix",
priority=10,
webhook_url="https://yourdomain.com/inbound/support",
)
# Update a route
client.update_inbound_route(route.id, webhook_url="https://yourdomain.com/inbound/v2", is_active=True)
# Catch-all route
client.create_inbound_route(
domain_id="domain-uuid",
pattern="*",
match_type="catch_all",
priority=100,
)
# List and delete
routes = client.list_inbound_routes()
client.delete_inbound_route(route.id)
Analytics
# Overview for the last 30 days
overview = client.get_analytics_overview(period="30d")
print(f"Delivery rate: {overview.delivery_rate}%")
# Custom date range
custom = client.get_analytics_overview(from_date="2025-01-01", to_date="2025-01-31")
# Time series data
timeseries = client.get_analytics_timeseries(period="7d", metrics=["sent", "delivered", "bounced"])
# Per-domain breakdown
domains = client.get_analytics_domains(period="30d", limit=10)
# Export as CSV
csv_data = client.export_analytics_csv(period="30d")
Account
account = client.get_account()
print(f"Plan: {account.plan}, Used: {account.emails_sent_this_month}/{account.monthly_quota}")
# Export all account data (GDPR)
export_data = client.export_account()
# Delete account permanently
client.delete_account()
Audit Logs
logs = client.list_audit_logs(page=1, per_page=50)
for log in logs.data:
print(f"{log.created_at}: {log.action} on {log.resource_type}")
Dead Letters
# List failed emails
dead_letters = client.list_dead_letters(count=20)
# Retry delivery
client.retry_dead_letter("dead-letter-uuid")
# Remove permanently
client.delete_dead_letter("dead-letter-uuid")
Error Handling
All API errors raise typed exceptions:
from euromail import (
EuroMail,
EuroMailError,
AuthenticationError,
RateLimitError,
ValidationError,
)
try:
client.send_email(...)
except AuthenticationError:
# Invalid or missing API key (401)
print("Check your API key")
except ValidationError as e:
# Invalid request parameters (422)
print(f"{e.code}: {e.message}")
except RateLimitError as e:
# Too many requests (429)
print(f"Retry after {e.retry_after} seconds")
except EuroMailError as e:
# Other API errors (4xx/5xx)
print(f"[{e.status}] {e.code}: {e.message}")
| Exception | HTTP Status | Description |
|---|---|---|
AuthenticationError |
401 | Invalid or missing API key |
ValidationError |
422 | Invalid request parameters |
RateLimitError |
429 | Too many requests (includes retry_after) |
EuroMailError |
4xx/5xx | Base class for all API errors |
API Reference
| Category | Method | Description |
|---|---|---|
| Emails | send_email(...) |
Send a single email |
send_batch(emails=...) |
Send up to 500 emails in one request | |
get_email(id) |
Get email details and status | |
list_emails(...) |
List emails with pagination and status filter | |
| Templates | create_template(...) |
Create an email template |
get_template(id) |
Get template by ID | |
update_template(id, ...) |
Update template fields | |
delete_template(id) |
Delete a template | |
list_templates(...) |
List templates with pagination | |
| Domains | add_domain(domain) |
Register a sending domain |
get_domain(id) |
Get domain details and DNS records | |
verify_domain(id) |
Trigger DNS verification | |
delete_domain(id) |
Remove a domain | |
list_domains(...) |
List domains with pagination | |
| Webhooks | create_webhook(...) |
Subscribe to events |
get_webhook(id) |
Get webhook details | |
update_webhook(id, ...) |
Update URL, events, or status | |
test_webhook(id) |
Send a test event | |
delete_webhook(id) |
Remove a webhook | |
list_webhooks(...) |
List webhooks with pagination | |
| Suppressions | add_suppression(email, ...) |
Suppress an email address |
delete_suppression(email) |
Remove a suppression | |
list_suppressions(...) |
List suppressions with pagination | |
| Contact Lists | create_contact_list(...) |
Create a contact list |
get_contact_list(id) |
Get list details | |
update_contact_list(id, ...) |
Update list settings | |
delete_contact_list(id) |
Delete a list | |
list_contact_lists() |
List all contact lists | |
add_contact(list_id, ...) |
Add a contact to a list | |
bulk_add_contacts(list_id, ...) |
Add multiple contacts | |
list_contacts(list_id, ...) |
List contacts with filters | |
remove_contact(list_id, email) |
Remove a contact | |
| Inbound | list_inbound_emails(...) |
List received emails |
get_inbound_email(id) |
Get inbound email details | |
delete_inbound_email(id) |
Delete an inbound email | |
| Inbound Routes | create_inbound_route(...) |
Create a routing rule |
get_inbound_route(id) |
Get route details | |
update_inbound_route(id, ...) |
Update a route | |
delete_inbound_route(id) |
Delete a route | |
list_inbound_routes(...) |
List routes with pagination | |
| Analytics | get_analytics_overview(...) |
Aggregated delivery stats |
get_analytics_timeseries(...) |
Daily metrics over time | |
get_analytics_domains(...) |
Per-domain breakdown | |
export_analytics_csv(...) |
Export stats as CSV | |
| Audit Logs | list_audit_logs(...) |
List account activity |
| Dead Letters | list_dead_letters(...) |
List permanently failed emails |
retry_dead_letter(id) |
Retry delivery | |
delete_dead_letter(id) |
Remove from dead letter queue | |
| Account | get_account() |
Get account info and quota |
export_account() |
Export all account data | |
delete_account() |
Permanently delete account |
Agent Mailboxes
Agent mailboxes provide persistent email addresses for AI agents with at-least-once message delivery via a lease/ack/nack model. The SDK wraps the full flow — create, long-poll, ack/nack — with typed dataclasses.
from euromail import EuroMail
client = EuroMail()
# Create a mailbox (auto-assigns a local_part if omitted)
mailbox = client.create_mailbox(display_name="Support Agent")
print(mailbox.address)
while True:
# Long-poll for the next message. Returns None on HTTP 408
# (no message arrived before the server-side timeout elapsed).
leased = client.wait_for_next_message(mailbox.id, timeout=30)
if leased is None:
continue
msg = leased.data
try:
handle(msg)
except Exception:
# Nack returns the message to the queue for redelivery.
client.nack_message(mailbox.id, msg.id, leased.lease_token)
raise
# Ack when done — message will not be redelivered.
client.ack_message(mailbox.id, msg.id, leased.lease_token)
The async client mirrors the sync API:
import asyncio
from euromail import AsyncEuroMail
async def main():
async with AsyncEuroMail() as client:
mailbox = await client.create_mailbox(display_name="Support Agent")
leased = await client.wait_for_next_message(mailbox.id, timeout=30)
if leased:
await client.ack_message(mailbox.id, leased.data.id, leased.lease_token)
asyncio.run(main())
Tip: when using wait_for_next_message(timeout=N), make sure the client's HTTP
timeout exceeds N seconds (pass timeout= when constructing EuroMail /
AsyncEuroMail), otherwise httpx will raise before the server responds.
Beyond the lease/ack/nack loop, the SDK exposes the rest of the mailbox API (all mirrored on the async client):
# Reply to a received message (enqueued through the normal send pipeline).
reply = client.reply_to_message(mailbox.id, msg.id, text_body="On it!")
# Threads: list the latest message per thread, or fetch a full thread.
threads = client.list_mailbox_threads(mailbox.id)
conversation = client.get_mailbox_thread(mailbox.id, threads[0].thread_id)
# Full-text search across subject + body.
hits = client.search_mailbox_messages(mailbox.id, "invoice")
# Labels (full replace, not merge).
labels = client.update_message_labels(mailbox.id, msg.id, ["urgent", "billing"])
# Pre-signed attachment download URLs (1-hour expiry).
attachments = client.get_message_attachment_urls(mailbox.id, msg.id)
# Contacts seen by this mailbox, and summary analytics.
contacts = client.list_mailbox_contacts(mailbox.id)
stats = client.get_mailbox_analytics(mailbox.id)
# Configure the auto-responder.
client.update_auto_responder(
mailbox.id,
enabled=True,
rules=[{"match": "*", "action": {"reply_text": "Out of office"}}],
)
See the Agent Mailboxes guide for the full flow, duplicate handling, and horizontal scaling patterns.
Requirements
- Python 3.10+
- httpx >= 0.27
License
MIT
Metadata
Release files for euromail 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| euromail-0.5.1.tar.gz | 46.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| euromail-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.4 kB
Release files / euromail-0.5.1.tar.gz
| Download URL | euromail-0.5.1.tar.gz |
|---|---|
| Size | 46.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fdff12cf6172416a8b13e1b9a4f34c61ce069ffbf9466d732d5d292cdc9c62e1
|
|
BLAKE2b-256 checksum How to use checksums |
60d3e16e14656ac10db7c285b4d72991fe34fc20ce63d1472ef74ca8f808295a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / euromail-0.5.1-py3-none-any.whl
| Download URL | euromail-0.5.1-py3-none-any.whl |
|---|---|
| Size | 30.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fc21c0e46d8e5f3f34000dfe8dd1ee618d1faf3c6622e6d617bdb4c22fb6e1f
|
|
BLAKE2b-256 checksum How to use checksums |
f00c675cf2f4cae5f23e49717a32dd8edb033fa6fe0b78b2d869c25ce439e9f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log