An inbox looks like the easiest document intake channel because every business already has one. Give suppliers, borrowers or claimants an address, watch attachments arrive, and send them to extraction. The first demo takes an afternoon. The first production incident arrives when the same invoice is forwarded twice, a reply carries the full attachment history, a mailbox retries delivery after a timeout, or a password-protected file enters a queue that has no honest state for it.

The market has moved past “read the attachment.” Rossum describes intake from email followed by validation, exception handling and writes to downstream systems. Docsumo describes shared-inbox triage that scans message bodies and attachments, tags threads by intent and routes work onward. Those are useful product signals, but the implementation contract still belongs to the team operating the workflow. The difficult part is not OCR. It is deciding what one business case is, which bytes have already been processed, and what evidence survives when the happy path does not.

This is the engineering layer behind email document workflow automation: a durable intake record, explicit identities, a state machine, security gates and an audit trail. Build those first and extraction becomes one stage. Skip them and the inbox remains the system of record by accident.

The email is an event, not the case

A single case can span ten emails. A single email can carry documents for three cases. A forwarded thread can contain a new file, three old files and an inline logo repeated in every signature. That is why the intake model needs separate identities for the transport event, the conversation, the attachment and the business case.

{
  "intake_id": "in_01J6...",
  "tenant_id": "tn_acme",
  "channel": "email",
  "mailbox": "ap@customer.example",
  "message": {
    "provider_id": "AAMkAG...",
    "internet_message_id": "<20260828.1422@example.org>",
    "in_reply_to": "<20260827.0914@example.org>",
    "references": ["<20260820.1610@example.org>"],
    "received_at": "2026-08-28T14:22:31Z"
  },
  "attachments": [{
    "attachment_id": "att_01J6...",
    "source_name": "invoice-3941.pdf",
    "sha256": "2b22c8...",
    "bytes": 248109,
    "detected_type": "application/pdf",
    "security_state": "accepted"
  }],
  "case_binding": { "state": "candidate", "case_id": null },
  "state": "received",
  "schema_version": "cogneris.email-intake/1"
}

RFC 5322 defines Message-ID as the machine-readable identifier for one version of one message, with In-Reply-To and References carrying reply relationships. Preserve those fields, but do not pretend they solve identity alone. Some senders omit them, forwarding can retain or rewrite them, and the mailbox provider has its own immutable identifier. A robust record stores both and records how any fallback key was derived.

Make delivery idempotent before extraction

Mailbox APIs, queues and webhooks are at-least-once systems in practice. A worker can complete the expensive work and crash before acknowledging it. A provider can retry after a timeout even though the first call succeeded. The correct response to a repeated delivery is not “run it again carefully.” It is to recognize a previously accepted intake atomically and return its existing result.

  • Use a tenant-scoped delivery key — combine mailbox identity with the provider message ID. A globally unique-looking ID is not a license to cross tenant boundaries.
  • Persist before dispatch — create the intake row and its uniqueness constraint before sending attachment work to a queue. A cache entry written later cannot close the race.
  • Separate retry from replay — retry resumes a failed stage on the same intake; replay creates an explicitly authorized new run with a reason, actor and new processing version.
  • Return the existing receipt — a duplicate delivery should resolve to the original intake ID, not disappear as a silent no-op. Operations needs to see that the duplicate happened.
  • Version side effects — a webhook delivery, ERP write and outbound reply each needs its own idempotency key. Making intake idempotent does not make the rest of the workflow idempotent.

The boundary is the transaction that inserts the intake record. If two workers can both cross it, every downstream deduplication step is cleanup.

Threading and deduplication are different decisions

Threading asks which messages belong to one conversation. Deduplication asks whether two attachment payloads represent bytes or business facts already processed. They overlap, but merging them creates dangerous shortcuts: “same thread” does not mean “same document,” and “same file hash” does not mean “same case.”

SignalSafe useUnsafe conclusion
Provider conversation IDCandidate conversation grouping inside one mailbox.All attachments belong to the same business case.
References / In-Reply-ToBuild an explainable reply graph when headers are present.The latest message replaces every earlier attachment.
SHA-256 of raw bytesDetect exact attachment repeats without opening the file.Two byte-distinct PDFs contain different invoices.
Normalized document fingerprintFlag re-encoded or rescanned candidates for policy review.The match is certain enough to discard automatically.
Business key after extractionDetect a repeated invoice number, claim number or applicant packet.The later document is fraudulent or irrelevant.

Keep the layers. First, suppress exact transport duplicates. Second, attach every unique payload to a candidate case. Third, run content-level matching as a business rule with an explainable outcome such as possible_duplicate. A corrected invoice may share its invoice number with the original and differ precisely because it must replace it. That is an exception, not garbage collection.

Treat every attachment as hostile input

Email weakens the usual assumption that an authenticated user intentionally uploaded a file. The sender can be spoofed, a trusted mailbox can be compromised, and a benign-looking document can exploit the parser that opens it. Security must run before document interpretation, in an isolated path that cannot execute active content or expose untrusted files from a public URL.

The OWASP File Upload Cheat Sheet recommends defense in depth: allow only required extensions, verify type rather than trusting the supplied Content-Type, generate storage names, limit file size, store outside the webroot, and use antivirus, sandboxing or content disarm and reconstruction where applicable. For an inbox pipeline, apply those controls before any OCR, preview rendering or office-format conversion.

  • Quarantine first — write immutable bytes to tenant-scoped quarantine storage with no public route and no execute permission.
  • Detect from content — retain the sender's filename and MIME claim as evidence, but route from verified signature and parser-safe type detection.
  • Bound every expansion — cap message size, attachment count, decompressed bytes, page count and parser time. A 4 MB archive can still be an availability incident.
  • Scan before render — thumbnailing, OCR and metadata extraction all parse attacker-controlled bytes. A preview is not a harmless read.
  • Isolate tenant and stage — quarantine, accepted source, derived pages and extracted JSON should have separate access policies and retention states.
  • Record the security verdict — engine, signature version, timestamp and outcome belong in the audit trail; “scanned” without a version cannot be reproduced.

Model a workflow, not a chain of functions

A production pipeline needs named states because every state can outlive the process that created it. “The worker is running” is not a durable fact. “Attachment accepted, classification pending” is. State should advance through compare-and-set transitions, with the input and output of each stage bound to versions.

StateEvidence required to enterTypical next action
ReceivedRaw message reference and immutable transport receipt.Enumerate attachments and persist identities.
QuarantinedStored bytes, hash, claimed type and size bounds.Type detection and security scanning.
AcceptedSecurity verdict and verified media type.Render, split and classify.
ClassifiedDocument type, model version, confidence and page bindings.Select extraction schema.
ExtractedTyped fields, citations, model and schema versions.Validate business rules and cross-document consistency.
Needs reviewExplicit exception code and the evidence that triggered it.Human correction, request for missing documents or rejection.
DeliveredDestination receipt, payload hash and idempotency key.Notify the sender and close the case step.

Keep message acknowledgement separate from business completion. The mailbox can be acknowledged after the durable intake exists, even if the case waits three days for a missing attachment. Otherwise one long-running business exception turns into repeated delivery and duplicate work.

Exceptions need codes, owners and clocks

“Manual review” is not one state. It is a holding area that hides whether the system lacks a password, cannot identify a case, detected malware, found conflicting values, or timed out writing to the ERP. Each condition needs a stable code, a responsible role, an SLA clock and an allowed resolution.

  • attachment_rejected — unsupported or unsafe bytes; security owns the policy and the file never reaches extraction.
  • password_required — an encrypted document is intact but unreadable; operations requests a safe replacement without asking for a password over the same thread.
  • case_unresolved — the message cannot be bound confidently; intake operations selects or creates the case.
  • possible_duplicate — content or business keys overlap; a reviewer chooses ignore, replace, merge or retain both.
  • validation_conflict — extracted facts disagree across documents or master data; the domain owner resolves the policy, not the OCR team.
  • delivery_failed — extraction is complete but the destination did not acknowledge the idempotent write; integration operations retries the delivery stage only.

An exception should never require rereading the whole thread to discover why it exists. Put the relevant message, attachment, page citation, validation rule and last attempted action on the exception record.

The audit trail should reconstruct one outcome

An audit trail is useful when it answers a concrete question: why did this attachment become this record in this system at this time? A list of generic application logs cannot answer that unless every event shares stable identities and hashes.

  1. Bind transport to bytes — mailbox, provider message ID, internet message ID, attachment ID, raw hash and immutable storage reference.
  2. Bind bytes to interpretation — detected type, security verdict, render version, page images, classification and extraction versions.
  3. Bind interpretation to decision — validation rules, confidence, citations, exception codes, reviewer identity and corrections.
  4. Bind decision to side effect — destination, payload hash, idempotency key, attempt history and acknowledged receipt.
  5. Preserve the policy version — retention, deduplication, auto-approval and exception-routing rules change; record the version applied to this case.

The pattern is the same as an audit trail for non-deterministic extraction: keep enough context to replay the explanation without rerunning the model and hoping for the same output. If a reviewer corrected a value, the original and corrected values both remain, with the page evidence behind the change.

A production-ready checklist

Before connecting an inbox to a system of record, prove these behaviours with fixtures and controlled failures.

  • Deliver the same message twice — the second delivery resolves to the first intake and produces no duplicate extraction or downstream write.
  • Forward a thread with repeated attachments — exact duplicates are recognized, new attachments are retained, and the case history stays explainable.
  • Send two byte-distinct versions of one invoice — the workflow raises a business duplicate or replacement decision instead of discarding either file.
  • Send a hostile, oversized and password-protected file — each stops in a distinct state before rendering, with a safe operator action.
  • Crash after extraction and before acknowledgement — the retry resumes the existing intake and does not repeat completed side effects.
  • Fail the destination after it accepts the payload — the same idempotency key returns the original receipt when delivery retries.
  • Trace one field end to end — start at the ERP record and reach the delivery receipt, reviewed value, extraction output, page region, attachment hash and source message.

The inbox is valuable precisely because customers and counterparties already know how to use it. Do not make them learn a portal before the workflow earns that demand. But do not confuse familiarity at the edge with simplicity underneath. A reliable inbox document processing system is a small event platform: explicit identity, durable state, bounded parsers, idempotent side effects and evidence that survives every exception.