---
title: "Validating and Downloading Raw Email"
canonical: "https://test.abhinandan.one/python-raw-email"
markdown_url: "https://test.abhinandan.one/python-raw-email.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
description: "Decode inline raw email bytes or verify a downloaded copy against email.content.raw.sha256 using primitive.decode_raw_email and verify_raw_email_download."
keywords: ["validate_email_received_event", "decode_raw_email", "verify_raw_email_download", "is_raw_included", "is_download_expired", "RawEmailDecodeError"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:03.428056+00:00"
source_files:
  - "sdk-python/src/primitive/webhook.py"
  - "sdk-python/tests/test_webhook.py"
sections:
  - {anchor: "validate-a-payload-against-the-schema", title: "Validate a payload against the schema"}
  - {anchor: "check-whether-the-raw-mime-source-is-inline", title: "Check whether the raw MIME source is inline"}
  - {anchor: "decode-the-inline-raw-email", title: "Decode the inline raw email"}
  - {anchor: "step-confirm-the-raw-content-is-inline", title: "Confirm the raw content is inline"}
  - {anchor: "step-decode-the-bytes", title: "Decode the bytes"}
  - {anchor: "step-handle-the-failure-modes", title: "Handle the failure modes"}
  - {anchor: "download-and-verify-a-non-inline-raw-email", title: "Download and verify a non-inline raw email"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Validating and Downloading Raw Email

Validate an email.received payload against the canonical JSON schema, then safely decode inline raw MIME bytes or verify a downloaded copy with SHA-256 hash checks.

Every `email.received` event carries the original MIME source of the message, either inlined as base64 in the payload or available at a time-limited download URL. Validate the payload against Primitive's JSON schema, then decode or download the raw bytes with a SHA-256 check so you never process corrupted or truncated mail.

You need this when you're building your own webhook receiver (bypassing [`handle_webhook`](https://test.abhinandan.one/python-webhook-verification.md)) and want to validate a payload independently, or when you need the original `.eml` bytes for archival, re-parsing, or forwarding as an attachment.

> **Note:** If you call `client.reply(...)` or `client.forward(...)` from the normalized [`ReceivedEmail`](https://test.abhinandan.one/python-receive-email.md) object, you don't need any of this, those flows already have what they need. Reach for the raw email only when you specifically need the original MIME bytes.

## Validate a payload against the schema

Call `validate_email_received_event(payload)`, which checks an arbitrary dict against the canonical `email.received` JSON schema and returns a fully-typed `EmailReceivedEvent` on success.

```python
from primitive import validate_email_received_event, WebhookValidationError

payload = {
    "id": "evt_0123...",
    "event": "email.received",
    "version": "2025-12-14",
    "email": {
        # ... full email.received shape
    },
}

try:
    event = validate_email_received_event(payload)
except WebhookValidationError as error:
    print(error.code)  # "SCHEMA_VALIDATION_FAILED"
    raise
```

`handle_webhook` and `handle_webhook_event` call this internally after verifying the signature, so in the normal webhook-handler path you never call it directly, see [Verifying Webhook Signatures](https://test.abhinandan.one/python-webhook-verification.md) and [Handling Webhook Events](https://test.abhinandan.one/python-webhook-events.md). Call it yourself only when you already have a parsed dict from somewhere else (a replay queue, a stored fixture, a different transport) and need the same validation and typed result `handle_webhook` gives you.

A malformed payload raises `WebhookValidationError` with `code == "SCHEMA_VALIDATION_FAILED"`, not a bare `ValueError` or a `pydantic.ValidationError`, catch that specific exception rather than a broad `Exception`.

## Check whether the raw MIME source is inline

Call `is_raw_included(event)`: it returns `True` when the raw email is inlined as base64 in the payload, and `False` when the message exceeded `email.content.raw.max_inline_bytes` and is only available at the download URL.

```python
from primitive import is_raw_included

if is_raw_included(event):
    print("raw bytes are inline in email.content.raw.data")
else:
    print("must fetch email.content.download.url instead")
```

`is_raw_included(event)` reads `event.email.content.raw.included` and accepts either a validated `EmailReceivedEvent` or a plain dict with that shape.

## Decode the inline raw email

Call `decode_raw_email(event)` to base64-decode `email.content.raw.data` and verify the result against `email.content.raw.sha256`, returning the original MIME bytes.

### 1. Confirm the raw content is inline

Call `is_raw_included(event)` first. If it returns `False`, skip to [Download and verify a non-inline raw email](#download-and-verify-a-non-inline-raw-email), `decode_raw_email` raises when the content isn't inline.

### 2. Decode the bytes

```python
from primitive import decode_raw_email, RawEmailDecodeError

try:
    raw_bytes = decode_raw_email(event)
except RawEmailDecodeError as error:
    print(error.code, str(error))
    raise
```

`decode_raw_email` base64-decodes `email.content.raw.data` and, by default, verifies the result against `email.content.raw.sha256` before returning it.

### 3. Handle the failure modes

`decode_raw_email` raises `RawEmailDecodeError` with one of these codes:

| Code | Cause |
|---|---|
| `NOT_INCLUDED` | The raw content isn't inline; the error's suggestion points at the download URL to fetch instead. |
| `INVALID_BASE64` | `email.content.raw.data` isn't valid base64. |
| `HASH_MISMATCH` | The decoded bytes don't match `email.content.raw.sha256`. Treat this as corrupted or truncated data, not something to retry blindly. |

On success, `raw_bytes` is a `bytes` object holding the exact MIME source of the message, headers first, ready to hand to an `.eml` parser or write to disk.

> **Tip:** Skip hash verification with `decode_raw_email(event, verify=False)` when you've already verified integrity elsewhere (for example, you're re-decoding the same event object twice in one process) and want to avoid the SHA-256 pass. Leave verification on by default everywhere else.

## Download and verify a non-inline raw email

Fetch `event.email.content.download.url`, then pass the response bytes to `verify_raw_email_download` to confirm the SHA-256 matches. The URL expires, so check `is_download_expired` first; there is no automatic hash check on an HTTP response you fetched yourself.

```python
import httpx
from primitive import is_download_expired, get_download_time_remaining, verify_raw_email_download

if is_download_expired(event):
    raise RuntimeError("download URL has expired; the webhook must be redelivered")

remaining_ms = get_download_time_remaining(event)
print(f"{remaining_ms}ms left to download")  # 0 once expired

response = httpx.get(event.email.content.download.url)
response.raise_for_status()

raw_bytes = verify_raw_email_download(response.content, event)
```

`verify_raw_email_download(downloaded, event)` hashes the bytes you give it with SHA-256 and compares against `email.content.raw.sha256`, raising `RawEmailDecodeError` with `code == "HASH_MISMATCH"` on any mismatch. It works on `bytes`, `bytearray`, or `memoryview`.

> **Warning:** Always call `verify_raw_email_download` on downloaded content before you parse or store it. A mismatch means the download was corrupted, truncated, or (in the worst case) tampered with in transit, never trust an unverified download URL response.

`is_download_expired` and `get_download_time_remaining` both read `email.content.download.expires_at` and default to comparing against the current time; pass an explicit `now` (milliseconds since epoch) in tests. `get_download_time_remaining` returns milliseconds and clamps to `0` once the URL has expired.
