Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /go-raw-email-downloads.md.
Verified · 8/11/2026

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 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.

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. 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. 2

    Decode and verify in one call#

    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. 3

    Skip verification only when you have a reason to#

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

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

Expected errors#

CodeCause
NOT_INCLUDEDemail.content.raw.included is false; the raw source must be downloaded instead. The error message includes the download URL.
INVALID_BASE64email.content.raw.data failed strict base64 decoding.
HASH_MISMATCHThe 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:

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.

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):

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:

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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer