Automating PDF Badge Delivery via AWS SES: Fixing MIME Boundary Corruption & Field Mapping Drift

Symptom Statement Link to this section

During high-throughput badge distribution, attendees receive PDFs that terminate mid-stream, render as garbled binary, or arrive with QR codes and barcodes shoved past the printable margin — and AWS SES SendRawEmail reports success the whole time. The email is delivered, the MessageId comes back clean, but the payload on the wire is corrupt. This is a delivery-transport failure, not a rendering failure: the badge composed correctly and only degraded once it was wrapped for email. This page is one of the delivery targets documented under PDF Routing Workflows, the routing stage of the Badge Generation & Template Sync pipeline, and it covers the exact SES path from validated PDF bytes to an inbox attachment that a Zebra or Dymo station will actually accept.

Root Cause Analysis Link to this section

Three concrete faults produce this class of silent degradation. Each sits at a different boundary — MIME construction, template synchronization, and printer hardware — and each fails quietly because SES only validates the envelope, never the attachment payload.

  1. MIME boundary corruption. Streaming raw PDF bytes into email.mime objects without explicit Content-Transfer-Encoding: base64 and strict CRLF (\r\n) line endings lets a stray 0x0A inside the binary body match a multipart boundary marker. SES treats that byte run as an early part terminator and silently drops every trailing byte, so the recipient gets a PDF that ends mid-object.
  2. Dynamic field mapping drift. Variable-length metadata — sponsor tiers, VIP designations, dietary tags — shifts layout coordinates when template versions are unpinned. A long field pushes scannable elements out of their bounding box, which directly degrades QR code generation and barcode threshold tuning output because the codes now straddle the non-printable edge. The upstream binding for those fields lives in dynamic field mapping.
  3. Label printer threshold violation. SES-delivered files that lack embedded DPI metadata or carry the wrong raster threshold are rejected at the print station. A 300 DPI thermal printer expects an exact media box and a strict grayscale threshold; a page a few points off spec, or an image rasterized below the resolution floor, is refused at the spooler.
SES badge-delivery path from validated PDF bytes to inbox attachment with fallback Validated PDF bytes enter a Pre-flight Validation stage that checks the media box against the 288 by 432 point badge spec and enforces an 8 MB payload cap; off-spec or oversize files escalate on a dashed fault path down to a Dead-Letter Queue for async replay. Valid bytes flow to an RFC 2046 MIME build stage that base64-encodes the attachment and emits CRLF boundaries, then to SendRawEmail, which dispatches at up to 14 messages per second under a retry loop of three attempts with exponential backoff on Throttling. Success reaches the attendee inbox as a real attachment. When retries are exhausted, a dashed fault path escalates down to a Fallback stage that generates a presigned S3 URL and sends it as a plain-text email, so the attendee still receives a retrievable link in place of the attachment. delivery path fault / fallback escalation retry ×3 · exp backoff on Throttling ≤ 8 MB cap ≤ 14 msg/s bytes off-spec / oversize retries exhausted link, not attachment Validated PDF bytes Pre-flight Validation media box · 288×432 pt size ≤ 8 MB MIME Build RFC 2046 base64 + CRLF SendRawEmail envelope accepted MessageId returned Attendee inbox real attachment Dead-Letter Queue async replay Fallback presigned S3 URL plain-text email

Symptom-to-Resolution Matrix Link to this section

MIME Boundary Corruption Link to this section

Symptoms

  • Attendee PDFs open then error “unexpected end of file” or render only the first page object.
  • SendRawEmail returns a valid MessageId, so monitoring shows 100% delivery.
  • Occasional InvalidParameterValue when a boundary collision produces a malformed part header.

Root cause. The binary body contains bare \n bytes and no enforced transfer encoding, so SES’s MIME parser mistakes body bytes for structural markers and truncates the part.

Fix steps

  1. Attach the PDF with MIMEApplication, which base64-encodes the payload so no raw binary reaches the boundary parser.
  2. Set Content-Transfer-Encoding: base64 explicitly, guarding against any middleware that strips the default header.
  3. Serialize with msg.as_bytes() (not as_string()), which emits canonical \r\n line endings and a collision-free boundary token.

Dynamic Field Mapping Drift Link to this section

Symptoms

  • QR or barcode elements clip at the badge edge or fail scan validation intermittently.
  • Layout shifts appear only for records with long sponsor or company strings.
  • The same template renders correctly for short records and overflows for long ones.

Root cause. Unpinned template versions and unbounded field length let dynamic metadata exceed its bounding box, moving fixed-position scannable assets into the non-printable margin.

Fix steps

  1. Cap field length before render (MAX_FIELD_LENGTH) and log a truncation warning rather than allowing silent overflow.
  2. Pin the template version per event so a mid-event template edit cannot shift coordinates under an in-flight batch.
  3. Validate the finished page’s media box against the expected badge dimensions in points, and reject any PDF that drifts out of tolerance before it is ever wrapped for SES.

Label Printer Threshold Violation Link to this section

Symptoms

  • Zebra/Dymo stations reject SES-delivered files at the spooler with a media or resolution error.
  • Files print on desktop viewers but fail on thermal hardware.
  • Grayscale badges emerge faint or with dropped elements.

Root cause. The PDF lacks correct DPI metadata or its page geometry does not match the printer’s expected media box and grayscale threshold.

Fix steps

  1. Assert the page geometry in points — a 4×6 in badge is 288 × 432 pt, not pixels; the 300 DPI figure governs rasterized image resolution inside the PDF, not the page size.
  2. Reject any PDF outside the width/height tolerance window during pre-flight so a mis-sized file never reaches a print queue.
  3. Keep the fallback delivery path (presigned S3 link) available so an operator can re-fetch a spec-correct master if a station still refuses the attachment.

Minimal Working Implementation Link to this section

A single self-contained path: validate the badge PDF, build an RFC 2046-compliant MIME message with explicit base64 encoding and CRLF boundaries, dispatch through SES with backoff, and verify the returned MessageId. PDF dimensions are measured in points (1 pt = 1/72 inch), so a 4×6 in badge has a media box of 288 × 432 pt.

PYTHON
import io
import time
import base64
import logging
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from email.utils import formataddr

import boto3
from botocore.exceptions import ClientError
from pypdf import PdfReader

logger = logging.getLogger("badge.ses")

# --- Config -----------------------------------------------------------------
MAX_PDF_SIZE_BYTES = 8_000_000   # Conservative: SES allows 40 MB, but big
                                 # attachments (plus ~33% base64 overhead) hurt
                                 # deliverability. Cap well below the hard limit.
MAX_FIELD_LENGTH = 120           # Prevents dynamic-field layout overflow.

# Media box in points for a 4 in x 6 in badge (288 x 432 pt), with tolerance.
BADGE_WIDTH_PT = (280, 296)
BADGE_HEIGHT_PT = (424, 440)

ses_client = boto3.client("ses", region_name="us-east-1")


def validate_badge_pdf(pdf_bytes: bytes, attendee_name: str) -> None:
    """Pre-flight: reject oversized, multi-page, or mis-sized badge PDFs."""
    if len(pdf_bytes) > MAX_PDF_SIZE_BYTES:
        raise ValueError(f"PDF exceeds {MAX_PDF_SIZE_BYTES} bytes; SES will choke deliverability.")

    reader = PdfReader(io.BytesIO(pdf_bytes))
    if len(reader.pages) != 1:
        raise ValueError("Badge PDF must contain exactly one page.")

    if len(attendee_name) > MAX_FIELD_LENGTH:
        logger.warning("Attendee name over %d chars; layout may overflow.", MAX_FIELD_LENGTH)

    box = reader.pages[0].mediabox
    width_pt, height_pt = float(box.width), float(box.height)
    if not (BADGE_WIDTH_PT[0] <= width_pt <= BADGE_WIDTH_PT[1]
            and BADGE_HEIGHT_PT[0] <= height_pt <= BADGE_HEIGHT_PT[1]):
        raise ValueError(
            f"Media box {width_pt:.1f} x {height_pt:.1f} pt is off the 288 x 432 pt badge spec."
        )


def build_mime_badge_email(to_email, from_email, subject, pdf_bytes, filename) -> bytes:
    """RFC 2046-compliant raw MIME with explicit base64 + CRLF boundaries."""
    msg = MIMEMultipart("mixed")
    msg["From"] = formataddr(("Event Ops", from_email))
    msg["To"] = to_email
    msg["Subject"] = subject
    msg.preamble = "This is a multi-part message in MIME format."

    msg.attach(MIMEText("Please find your event badge attached.", "plain", "utf-8"))

    pdf_part = MIMEApplication(pdf_bytes, Name=filename)  # base64-encodes the body
    pdf_part["Content-Disposition"] = f'attachment; filename="{filename}"'
    pdf_part["Content-Transfer-Encoding"] = "base64"       # explicit, guards middleware
    msg.attach(pdf_part)

    return msg.as_bytes()  # emits \r\n line endings + a collision-free boundary


def send_badge_via_ses(raw_mime_bytes: bytes, max_retries: int = 3) -> dict:
    """Dispatch raw MIME to SES with exponential backoff on throttling."""
    for attempt in range(max_retries):
        try:
            resp = ses_client.send_raw_email(RawMessage={"Data": raw_mime_bytes})
            return {"status": "success", "message_id": resp["MessageId"]}
        except ClientError as exc:
            code = exc.response["Error"]["Code"]
            if code == "Throttling":
                time.sleep((2 ** attempt) * 0.5)
                continue
            if code == "InvalidParameterValue":
                raise RuntimeError(f"SES rejected payload: {exc.response['Error']['Message']}")
            raise
    raise RuntimeError("SES delivery failed after max retries due to throttling.")


if __name__ == "__main__":
    pdf = open("badge.pdf", "rb").read()
    validate_badge_pdf(pdf, "Ada Lovelace")
    raw = build_mime_badge_email(
        "[email protected]", "[email protected]",
        "Your event badge", pdf, "badge.pdf",
    )
    result = send_badge_via_ses(raw)

    # Verification: a real MessageId, and the attachment survives a base64 round-trip.
    assert result["message_id"], "SES returned no MessageId"
    del pdf  # free the raw bytes before the next high-concurrency iteration
    print("Delivered:", result["message_id"])

Memory & Performance Constraints Link to this section

Resource Constraint Mitigation
Attachment size Base64 inflates the PDF ~33%; oversized mail hurts inbox acceptance Cap raw PDF at 8 MB even though SES permits 40 MB per message
Per-badge memory Raw bytes + base64 copy + MIME buffer coexist during construction Generate to bytes, encode once, del pdf_bytes after as_bytes(); keep peak under 15 MB/badge
SES send rate Default sending quota is ~14 messages/second in most regions Token-bucket rate limiting; publish bounce/complaint metrics via SES configuration sets to CloudWatch
Worker heap under load High-concurrency workers fragment the heap during peak registration Explicit del of raw bytes plus bounded worker concurrency; offload retries to async batch processing

Incident Triage & Rollback Link to this section

When corruption or drift is detected in production, contain first and diagnose second — the target is under 15 minutes to a clean fallback.

  1. Circuit-break the SES path. Disable automated SendRawEmail dispatch in the routing orchestrator and switch to the presigned-URL fallback so attendees keep receiving retrievable badges.
  2. Confirm the fault class. Pull one failing message and check whether the attachment survives a base64 round-trip locally (base64 -d the part body and open it) — a truncated decode confirms MIME corruption rather than a bad source PDF.
  3. Pin the template. Revert to the last known-good template version and disable dynamic field injection until bounding-box tolerances are re-validated.
  4. Serve the fallback link. Dispatch a plain-text email carrying a presigned S3 URL, bypassing MIME boundary risk entirely during the incident:
PYTHON
def fallback_delivery(s3_client, bucket: str, key: str) -> str:
    """Presigned GET URL — a link instead of an attachment, no MIME risk."""
    return s3_client.generate_presigned_url(
        "get_object",
        Params={"Bucket": bucket, "Key": key},
        ExpiresIn=86_400,  # 24h
    )

Rollback validation. Re-enable SES routing only after validate_badge_pdf() passes 100% of a test batch and send_badge_via_ses() returns consistent MessageId values with no InvalidParameterValue. Verify a sampled delivered attachment byte-for-byte against its source before restoring full throughput.

Frequently Asked Questions Link to this section

Why does SES report success while attendees get corrupt PDFs? SES validates the message envelope and headers, not the integrity of an attachment payload. A boundary collision truncates the body after SES has already accepted the message, so the MessageId is genuine even though the delivered bytes are incomplete. Enforcing base64 encoding and CRLF boundaries removes the collision at the source.

Should I use SendRawEmail or SendEmail for badge attachments? SendRawEmail (or the v2 SendEmail with a Raw content block) is required whenever you attach a file, because the templated APIs do not accept binary attachments. Raw sending is what gives you explicit control over the MIME structure that prevents boundary corruption in the first place.

Is the 4×6 badge 288 points or 1200 pixels? Both, at different layers. The PDF page geometry is 288 × 432 pt (points, at 72 pt/inch), which is what the printer’s media box check reads. The 300 DPI figure governs the resolution of rasterized images placed inside the page — a 4-inch-wide image at 300 DPI is 1200 px — and never the page dimensions themselves. Validating the media box in points is what catches mis-sized files.

  • PDF Routing Workflows — the parent stage that owns delivery contracts and fallback chains; this SES path is one of its transport targets.
  • QR Code Generation — the upstream encoder whose output is exactly what drifts out of margin when dynamic fields overflow the layout.
  • Barcode Threshold Tuning — the sibling stage that calibrates 1D barcode density so scannable elements survive the same printer thresholds referenced here.
  • Async Batch Processing — owns the dead-letter queue and retry machinery that quarantines badges this path cannot deliver after backoff is exhausted.