{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-raw-email","markdown_url":"https://test.abhinandan.one/python-raw-email.md","article":{"id":"cf3de3d8-f829-42b9-bcb3-8310adf3821b","article_slug":"python-raw-email","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:03.428056+00:00","keywords":["validate_email_received_event","decode_raw_email","verify_raw_email_download","is_raw_included","is_download_expired","RawEmailDecodeError"],"meta_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.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/webhook.py","sdk-python/tests/test_webhook.py"],"recording_id":null,"replayable":false,"task_name":"Validating and Downloading Raw Email","category":"Python SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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.\n\nYou need this when you're building your own webhook receiver (bypassing [`handle_webhook`](python-webhook-verification)) and want to validate a payload independently, or when you need the original `.eml` bytes for archival, re-parsing, or forwarding as an attachment.\n\n<Note>\n\nIf you call `client.reply(...)` or `client.forward(...)` from the normalized [`ReceivedEmail`](python-receive-email) 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.\n\n</Note>\n\n## Validate a payload against the schema\n\nCall `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.\n\n```python\nfrom primitive import validate_email_received_event, WebhookValidationError\n\npayload = {\n    \"id\": \"evt_0123...\",\n    \"event\": \"email.received\",\n    \"version\": \"2025-12-14\",\n    \"email\": {\n        # ... full email.received shape\n    },\n}\n\ntry:\n    event = validate_email_received_event(payload)\nexcept WebhookValidationError as error:\n    print(error.code)  # \"SCHEMA_VALIDATION_FAILED\"\n    raise\n```\n\n`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](python-webhook-verification) and [Handling Webhook Events](python-webhook-events). 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.\n\nA 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`.\n\n## Check whether the raw MIME source is inline\n\nCall `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.\n\n```python\nfrom primitive import is_raw_included\n\nif is_raw_included(event):\n    print(\"raw bytes are inline in email.content.raw.data\")\nelse:\n    print(\"must fetch email.content.download.url instead\")\n```\n\n`is_raw_included(event)` reads `event.email.content.raw.included` and accepts either a validated `EmailReceivedEvent` or a plain dict with that shape.\n\n## Decode the inline raw email\n\nCall `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.\n\n<Steps>\n\n<Step title=\"Confirm the raw content is inline\">\n\nCall `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.\n\n</Step>\n\n<Step title=\"Decode the bytes\">\n\n```python\nfrom primitive import decode_raw_email, RawEmailDecodeError\n\ntry:\n    raw_bytes = decode_raw_email(event)\nexcept RawEmailDecodeError as error:\n    print(error.code, str(error))\n    raise\n```\n\n`decode_raw_email` base64-decodes `email.content.raw.data` and, by default, verifies the result against `email.content.raw.sha256` before returning it.\n\n</Step>\n\n<Step title=\"Handle the failure modes\">\n\n`decode_raw_email` raises `RawEmailDecodeError` with one of these codes:\n\n| Code | Cause |\n|---|---|\n| `NOT_INCLUDED` | The raw content isn't inline; the error's suggestion points at the download URL to fetch instead. |\n| `INVALID_BASE64` | `email.content.raw.data` isn't valid base64. |\n| `HASH_MISMATCH` | The decoded bytes don't match `email.content.raw.sha256`. Treat this as corrupted or truncated data, not something to retry blindly. |\n\n</Step>\n\n</Steps>\n\nOn 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.\n\n<Tip>\n\nSkip 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.\n\n</Tip>\n\n## Download and verify a non-inline raw email\n\nFetch `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.\n\n```python\nimport httpx\nfrom primitive import is_download_expired, get_download_time_remaining, verify_raw_email_download\n\nif is_download_expired(event):\n    raise RuntimeError(\"download URL has expired; the webhook must be redelivered\")\n\nremaining_ms = get_download_time_remaining(event)\nprint(f\"{remaining_ms}ms left to download\")  # 0 once expired\n\nresponse = httpx.get(event.email.content.download.url)\nresponse.raise_for_status()\n\nraw_bytes = verify_raw_email_download(response.content, event)\n```\n\n`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`.\n\n<Warning>\n\nAlways 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.\n\n</Warning>\n\n`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.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Verifying Webhook Signatures\" href=\"python-webhook-verification\">\n\nVerify the HMAC or Standard Webhooks signature before you ever reach payload validation.\n\n</Card>\n\n<Card title=\"Receiving and Parsing Inbound Email\" href=\"python-receive-email\">\n\nGet the normalized ReceivedEmail object for the common send/reply/forward flow instead of raw bytes.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up every RawEmailDecodeError and WebhookValidationError code and its fix.\n\n</Card>\n\n<Card title=\"Python SDK Type Reference\" href=\"python-types-reference\">\n\nBrowse the generated EmailReceivedEvent dataclasses referenced by validate_email_received_event.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-python/src/primitive/webhook.py","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Validating+and+Downloading+Raw+Email&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-raw-email","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}