Skip to main content

Official Python SDK for the EuroMail transactional email service

Project description

euromail

Official Python SDK for the EuroMail transactional email service.

PyPI

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.from_address}, 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.9+
  • httpx >= 0.27

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

euromail-0.4.0.tar.gz (31.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

euromail-0.4.0-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file euromail-0.4.0.tar.gz.

File metadata

  • Download URL: euromail-0.4.0.tar.gz
  • Upload date:
  • Size: 31.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for euromail-0.4.0.tar.gz
Algorithm Hash digest
SHA256 252b9f14325aa160360824661304169203852eeabe7f68d71c41921e21ea1782
MD5 f7e867c5698fb6a2108a51305243df01
BLAKE2b-256 fd155a2204551007a5ae01154153a4d1b3eaff53e8823ad78dd1c28e458bf184

See more details on using hashes here.

Provenance

The following attestation bundles were made for euromail-0.4.0.tar.gz:

Publisher: publish.yml on kalle-works/euromail-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file euromail-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: euromail-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 27.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for euromail-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 29e19e0a84be39ef09c5a8f5b231abf0d4fb64ba3556cd77d630b0d07bcf3249
MD5 ac519429010436785e15add3b90d2f86
BLAKE2b-256 dc28207bb69ca5a635873ff2e95ed3307d467ba62eacb48899abeca22c956045

See more details on using hashes here.

Provenance

The following attestation bundles were made for euromail-0.4.0-py3-none-any.whl:

Publisher: publish.yml on kalle-works/euromail-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page