---
title: "Raw Email and Attachment Downloads"
canonical: "https://test.abhinandan.one/go-raw-email-downloads"
markdown_url: "https://test.abhinandan.one/go-raw-email-downloads.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Go SDK"
description: "DecodeRawEmail decodes and SHA-256-verifies inline raw MIME bytes from an EmailReceivedEvent in the Go SDK, with VerifyRawEmailDownload for downloaded content."
keywords: ["DecodeRawEmail", "VerifyRawEmailDownload", "IsRawIncluded", "IsDownloadExpired", "GetDownloadTimeRemaining", "email.content.raw"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:56.376067+00:00"
source_files:
  - "sdk-go/webhook.go"
sections:
  - {anchor: "check-whether-raw-content-is-inline", title: "Check whether raw content is inline"}
  - {anchor: "decode-inline-raw-content", title: "Decode inline raw content"}
  - {anchor: "step-confirm-the-content-is-inlined", title: "Confirm the content is inlined"}
  - {anchor: "step-decode-and-verify-in-one-call", title: "Decode and verify in one call"}
  - {anchor: "step-skip-verification-only-when-you-have-a-reason-to", title: "Skip verification only when you have a reason to"}
  - {anchor: "expected-errors", title: "Expected errors"}
  - {anchor: "download-large-raw-content", title: "Download large raw content"}
  - {anchor: "check-download-url-expiry", title: "Check download URL expiry"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Raw Email and Attachment Downloads

Decode inline raw email bytes with SHA-256 verification, or check whether a download URL for large content has expired, using the Go SDK's raw-email helpers.

Every `email.received` event carries the raw MIME source of the inbound message, either inlined as base64 or as a time-limited download URL when it's too large to inline. Use these helpers when you need the original bytes: verifying a DKIM signature yourself, archiving the source `.eml`, or re-parsing headers a normalized field doesn't expose.

For most handlers you don't need this at all, `primitive.Receive(...)` already gives you a [`ReceivedEmail`](https://test.abhinandan.one/go-receiving-webhooks.md) with the fields you need. Reach for the raw-email helpers only when you need the bytes themselves.

## Check whether raw content is inline

`primitive.IsRawIncluded(event)`, which reads `email.content.raw.included` off an `email.received` event, returns true when the raw MIME source is inlined and false when it must be downloaded.

```go
package main

import (
	"log"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

func inspect(event any) {
	included, err := primitive.IsRawIncluded(event)
	if err != nil {
		// malformed payload: email.content.raw.included is missing
		log.Fatal(err)
	}

	if included {
		raw, err := primitive.DecodeRawEmail(event)
		if err != nil {
			log.Fatal(err)
		}
		_ = raw
		return
	}
	// must download instead, see "Download large raw content" below
}
```

`IsRawIncluded` returns a `*primitive.WebhookPayloadError` (code `PAYLOAD_WRONG_TYPE`) if `email.content.raw.included` is missing from the payload, which only happens if you hand it something other than a well-formed `EmailReceivedEvent`.

## Decode inline raw content

`primitive.DecodeRawEmail(event)` base64-decodes `email.content.raw.data` and, by default, verifies the bytes against `email.content.raw.sha256`, returning the original MIME source as `[]byte`.

### 1. Confirm the content is inlined

`DecodeRawEmail` only works when `email.content.raw.included` is `true`. If it's `false`, it returns a `*primitive.RawEmailDecodeError` with code `NOT_INCLUDED`, whose message reports the raw size, the inline threshold, and the download URL. Check `IsRawIncluded` first if you're not sure.

### 2. Decode and verify in one call

```go
raw, err := primitive.DecodeRawEmail(event)
if err != nil {
    log.Fatal(err)
}
// raw is []byte: the original MIME source
```

By default `DecodeRawEmail` verifies the decoded bytes against `email.content.raw.sha256` and fails closed on a mismatch. This catches truncated or corrupted payloads before you act on bad data.

### 3. Skip verification only when you have a reason to

Pass `false` as the second argument to skip the hash check:

```go
raw, err := primitive.DecodeRawEmail(event, false)
```

### Expected errors

| Code | Cause |
|---|---|
| `NOT_INCLUDED` | `email.content.raw.included` is `false`; the raw source must be downloaded instead. The error message includes the download URL. |
| `INVALID_BASE64` | `email.content.raw.data` failed strict base64 decoding. |
| `HASH_MISMATCH` | The decoded bytes' SHA-256 doesn't match `email.content.raw.sha256` (only checked when verification is enabled). |

> **Warning:** `DecodeRawEmail` assumes a well-formed event from `primitive.Receive(...)` or `primitive.HandleWebhookEvent(...)`. Passing a hand-constructed event with `raw.included: true` but no `raw.data` produces undefined behavior, not a clean error.

## Download large raw content

When `email.content.raw.included` is `false`, the raw source exceeded the inline threshold and must be fetched from `email.content.download.url`, then checked with `primitive.VerifyRawEmailDownload(downloaded, event)`.

Fetch the URL from the event's `email.content.download.url` field with a standard HTTP client, then verify the bytes:

```go
package main

import (
	"io"
	"log"
	"net/http"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

func download(event any, downloadURL string) []byte {
	resp, err := http.Get(downloadURL)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	downloaded, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	verified, err := primitive.VerifyRawEmailDownload(downloaded, event)
	if err != nil {
		// *primitive.RawEmailDecodeError with code HASH_MISMATCH
		log.Fatal(err)
	}
	// verified == downloaded, but only returned after a passing SHA-256 check
	return verified
}
```

`VerifyRawEmailDownload` always checks the hash, there's no skip-verification option, since a downloaded payload has more transport surface (proxies, retries, partial reads) to corrupt it than an inline base64 field.

> **Tip:** Call `IsRawIncluded` before deciding whether to decode or download, rather than calling `DecodeRawEmail` and matching on the `NOT_INCLUDED` error. Branching on the boolean keeps the download URL read on the typed event instead of parsed out of an error message.

## Check download URL expiry

`primitive.IsDownloadExpired(event)` compares `email.content.download.expires_at` against now and returns true once the download URL has expired.

```go
package main

import (
	"log"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

func checkExpiry(event any) {
	expired, err := primitive.IsDownloadExpired(event)
	if err != nil {
		log.Fatal(err)
	}
	if expired {
		// the URL in email.content.download.url no longer works
		log.Println("download URL expired")
	}
}
```

To decide whether there's enough time left for a slow download, use `GetDownloadTimeRemaining`, which returns milliseconds remaining (`0` if already expired):

```go
remainingMs, err := primitive.GetDownloadTimeRemaining(event)
if err != nil {
    log.Fatal(err)
}
if remainingMs < 60_000 {
    // less than a minute left — fetch now or treat as expired
}
```

Both helpers accept an optional second argument (unix milliseconds) to override "now," which is useful in tests:

```go
expired, _ := primitive.IsDownloadExpired(event, fixedNowMillis)
remainingMs, _ := primitive.GetDownloadTimeRemaining(event, fixedNowMillis)
```

Both return a `*primitive.WebhookPayloadError` (code `PAYLOAD_WRONG_TYPE`) if `email.content.download.expires_at` is missing or isn't a valid RFC 3339 timestamp.
