Metadata-Version: 2.4
Name: maielr
Version: 1.0.0
Summary: Official Python SDK for the Maielr email API
Project-URL: Homepage, https://maielr.com
Project-URL: Documentation, https://docs.maielr.com
Project-URL: Repository, https://github.com/maielr/maielr-python
Project-URL: Issues, https://github.com/maielr/maielr-python/issues
Author-email: Maielr <sdk@maielr.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api,email,maielr,smtp,transactional-email
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: httpx>=0.24.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# Maielr Python SDK

Official Python SDK for the [Maielr](https://maielr.com) email API. Send transactional and marketing emails with enterprise-grade deliverability.

## Installation

```bash
pip install maielr
```

**Requirements:** Python 3.8+

## Quick Start

```python
from maielr import Maielr

client = Maielr("your_api_key")

result = client.send(
    from_email="sender@yourdomain.com",
    to="recipient@example.com",
    subject="Hello from Maielr",
    html="<h1>Hello World</h1>",
)

print(result)  # {"id": "...", "status": "queued", "message": "..."}
```

## Features

- **Sync and async clients** -- `Maielr` and `AsyncMailer`
- **Full type hints** (PEP 484) with `py.typed` marker
- **Automatic retries** with exponential backoff and jitter on 429 and 5xx errors
- **Structured error hierarchy** with request IDs for debugging
- **Webhook signature verification** with HMAC-SHA256 and replay attack prevention
- **Auto-pagination iterators** for list endpoints
- **Idempotency keys** to prevent duplicate operations
- **Per-request timeouts** to override the global timeout
- **Debug logging** with configurable verbosity
- **Custom HTTP client** support for advanced configurations
- **Security headers** including SDK version and request ID tracking
- **Thread-safe** synchronous client
- **Python 3.8+** compatible

## Authentication

Get your API key from the [Maielr Dashboard](https://app.maielr.com/developer/api-keys).

```python
client = Maielr("your_api_key")
```

## Configuration

```python
client = Maielr(
    api_key="your_api_key",
    base_url="https://your-custom-relay.example.com",  # custom base URL
    timeout=60.0,       # 60 second timeout (default: 30s)
    max_retries=5,      # 5 retries (default: 3)
    debug=True,         # enable debug logging (default: False)
)
```

### Context Manager

```python
with Maielr("your_api_key") as client:
    client.send(...)
# Client is automatically closed
```

## Sending Email

```python
result = client.send(
    from_email="sender@yourdomain.com",
    from_name="Acme Support",
    to=["alice@example.com", "bob@example.com"],
    cc="manager@example.com",
    bcc=["archive@example.com"],
    subject="Monthly Report",
    html="<h1>Your Report</h1><p>See attached.</p>",
    text="Your Report -- see attached.",
    reply_to="support@yourdomain.com",
    headers={"X-Campaign-Id": "monthly-report"},
    attachments=[
        {
            "filename": "report.pdf",
            "content": "<base64-encoded-content>",
            "content_type": "application/pdf",
        }
    ],
)
```

## Idempotency Keys

Prevent duplicate sends by passing an idempotency key. If the server has already processed a request with the same key, it returns the original response instead of creating a duplicate.

```python
# Generate a key
idem_key = Maielr.generate_idempotency_key()

# Pass it to any POST method
result = client.send(
    from_email="sender@yourdomain.com",
    to="recipient@example.com",
    subject="Important",
    html="<p>This will only be sent once.</p>",
    idempotency_key=idem_key,
)

# Also works with add_suppression and batch_send
client.add_suppression("user@example.com", idempotency_key=idem_key)
```

## Per-Request Timeouts

Override the global timeout for individual requests:

```python
# This request gets 5 seconds instead of the default 30
result = client.send(
    from_email="sender@yourdomain.com",
    to="recipient@example.com",
    subject="Quick",
    html="<p>Hurry</p>",
    timeout=5.0,
)

# Works on all methods
status = client.get_status("msg_123", timeout=10.0)
bounces = client.list_bounces(days=7, timeout=60.0)
```

## Auto-Pagination

Iterate over all results without manual pagination:

```python
# Sync -- generator that yields individual messages
for msg in client.list_messages_auto(status="delivered", page_size=50):
    print(msg["id"], msg["status"])

# Async -- async iterator
async for msg in await async_client.list_messages_auto(page_size=50):
    print(msg["id"])
```

## Webhook Signature Verification

Verify incoming webhook payloads to ensure they are authentic and not replayed:

```python
from maielr import Maielr, WebhookVerificationError

client = Maielr("your_api_key")

# In your webhook handler (e.g., Flask, FastAPI, Django)
def handle_webhook(request):
    payload = request.body  # raw bytes
    signature = request.headers["X-Maielr-Signature"]
    webhook_secret = "whsec_your_webhook_secret"

    try:
        # Verify + parse in one step
        event = client.webhooks.construct_event(
            payload=payload,
            signature=signature,
            secret=webhook_secret,
            tolerance=300,  # reject signatures older than 5 minutes
        )
        print(f"Received event: {event['type']}")
    except WebhookVerificationError as e:
        print(f"Verification failed: {e.message}")
        return 400

    # Or verify only (returns True or raises)
    client.webhooks.verify_signature(payload, signature, webhook_secret)
```

The signature format is `t=<unix_timestamp>,v1=<hmac_sha256_hex>`. The signed content is `<timestamp>.<raw_payload>`.

## Debug Logging

Enable debug mode to see request/response details:

```python
import logging

# Option 1: Use the debug flag
client = Maielr("your_api_key", debug=True)

# Option 2: Configure the logger directly
logging.getLogger("maielr").setLevel(logging.DEBUG)
logging.getLogger("maielr").addHandler(logging.StreamHandler())
```

Debug output includes:
- Request method and URL
- Attempt number and request ID
- Response status code and timing
- Retry decisions and delays

## Custom HTTP Client

Pass a pre-configured `httpx.Client` for advanced scenarios (proxies, custom certificates, connection pooling):

```python
import httpx

custom_client = httpx.Client(
    proxies="http://proxy.example.com:8080",
    verify="/path/to/custom-ca-bundle.crt",
    limits=httpx.Limits(max_connections=50),
)

client = Maielr(
    api_key="your_api_key",
    http_client=custom_client,
)

# The SDK adds auth headers but uses your client for transport
result = client.send(...)

# When done, close your custom client separately
custom_client.close()
```

For async:

```python
import httpx

custom_async = httpx.AsyncClient(
    proxies="http://proxy.example.com:8080",
)

client = AsyncMailer(
    api_key="your_api_key",
    http_client=custom_async,
)
```

## Async Usage

Every method is available asynchronously via `AsyncMailer`:

```python
import asyncio
from maielr import AsyncMailer

async def main():
    async with AsyncMailer("your_api_key") as client:
        result = await client.send(
            from_email="sender@yourdomain.com",
            to="recipient@example.com",
            subject="Async Hello",
            html="<h1>Hello from async!</h1>",
            idempotency_key=AsyncMailer.generate_idempotency_key(),
        )

        # Auto-pagination works async too
        async for msg in await client.list_messages_auto():
            print(msg["id"])

asyncio.run(main())
```

## Error Handling

```python
from maielr import (
    Maielr,
    MailerError,
    AuthError,
    RateLimitError,
    ValidationError,
    NotFoundError,
)

client = Maielr("your_api_key")

try:
    result = client.send(
        from_email="sender@yourdomain.com",
        to="recipient@example.com",
        subject="Hello",
        html="<h1>Hello</h1>",
    )
except AuthError as e:
    print(f"Auth failed: {e.message}")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except ValidationError as e:
    print(f"Validation error: {e.message}")
    print(f"Details: {e.errors}")
except NotFoundError as e:
    print(f"Not found: {e.message}")
except MailerError as e:
    print(f"API error ({e.status_code}): {e.message}")
    print(f"Request ID: {e.request_id}")  # client-generated or server-assigned
```

### Error Hierarchy

```
MailerError (base)
  APIError              (unmapped status codes)
  AuthError             (401, 403)
  ValidationError       (400)
  NotFoundError         (404)
  RateLimitError        (429)
  ServerError           (5xx)
  ConnectionError       (network failures)
  TimeoutError          (request timeouts)

WebhookVerificationError  (signature verification failures)
```

All errors include a `request_id` attribute -- either the server-assigned ID from the `x-request-id` response header or the client-generated `X-Request-ID` sent with the request.

## Retry Behavior

The SDK automatically retries on:
- **HTTP 429** -- Rate limit exceeded (respects `Retry-After` header)
- **HTTP 500, 502, 503, 504** -- Server errors
- **Network connection failures**
- **Request timeouts**

Retries use exponential backoff with jitter:

```
delay = base_delay * (2 ^ attempt) + random(0, base_delay)
```

Where `base_delay` is 1 second. The delay is capped at 30 seconds. For 429 responses, the `Retry-After` header takes precedence.

## Security Headers

Every request automatically includes:
- `X-SDK-Version: maielr-python/1.0.0` -- SDK identification
- `X-SDK-Timestamp: <ISO 8601>` -- Request timestamp
- `X-Request-ID: <uuid4>` -- Unique request identifier for tracing

## Thread Safety

The synchronous `Maielr` client is thread-safe because it uses `httpx.Client`, which is designed for concurrent use across threads.

The `AsyncMailer` client should be used within a single asyncio event loop. For multi-threaded async applications, create one `AsyncMailer` per event loop.

## License

MIT
