Blog articles

How We Built Email Simulations

By Lorenzo Taylor · Founding MTS at Bluejay

Customers reach a business on every channel it offers, and AI agents are now answering on all of them. Bluejay already tests those agents on voice, chat and SMS. Email is one of the busiest support channels there is, and more of each inbox is being answered by an AI agent every month. Those agents deserve the same testing as the ones on the phone.

Today, testing an email agent usually means posting text to the model through a webhook. That checks the writing. It misses everything between a customer's inbox and the agent, which is where most email problems happen. So we built email as a full channel in Bluejay. This post covers the architecture and the decisions behind it.

A few terms come up throughout. A Digital Human is a simulated customer with a name, a goal and a way of writing. A simulation runs a set of Digital Humans against one agent. Our running example is Lumen, an online furniture store whose support agent answers [email protected], and Maria, a Digital Human whose lamp never arrived.

Why a webhook test is not enough

Only one common email failure is about what the agent wrote.

FailureWhat happens to MariaWebhook testBluejay
Wrong answerShe gets the wrong refund policy.✓✓
QuarantineHer email is held by a filter. Nobody answers.✗✓
SpamThe reply goes to her spam folder.✗✓
BounceThe reply never arrives.✗✓
Wrong senderThe reply comes from an address she does not recognize.✗✓
Slow replyShe waits an hour for a two minute answer.✗✓

What it had to do

Architecture

Email simulations reuse most of what already powers voice and SMS. The Digital Human engine, the grading pipeline and the uptime monitors are shared. What we added is a mail layer with an outbound path and an inbound path.

Example: one turn of Maria's conversation. The top row sends her email. The third row brings Lumen's reply back. Dashed lines are mail crossing between us and Lumen.

OUTBOUND Digital HumanWrites the email Send queueOne job per turn Send workerPaced and retried Mail servicemail.bluejaysims.com Lumen's agentReads and replies INBOUND Inbound mailStores the raw reply NotificationSigned, verified RouterTrust check, thread ReaderPulls out the reply Grading and resultsDelivery checks on every reply. Content grade at the end. Dashboards and uptime alerts. Next turn

Key decisions

Run our own mail stack

Our first version sent through an email API from one shared sender address. It worked for a single test and broke down at scale in two ways. The provider's rate limit applied to the whole account, so parallel conversations throttled each other. And one shared address pooled every customer's sending reputation into one place we could not control.

We compared the options against what the product needed.

OptionWhat decided it
Email API, one shared senderAccount-wide rate limits and one pooled reputation.
Agent inbox startupsYoung, no uptime guarantee, and we would still warm every inbox ourselves.
Rented inboxes from cold email vendorsTheir servers stamp their own authentication results, so the verdicts we grade on could not be trusted.
A cloud mail service on our own domainSends and receives raw mail, includes the receiving server's verdicts, and reports bounces and complaints. We chose this.

One permanent address per Digital Human

All test mail comes from mail.bluejaysims.com, a subdomain we keep for testing. We never send tests from getbluejay.ai or from a customer's domain, so a test can never hurt anyone's real reputation. Customers allowlist it once.

Each Digital Human gets a permanent address on that domain, just like the phone number it uses on voice and SMS tests. The number in the address identifies the Digital Human and never changes.

Our first design gave every conversation a fresh address. That broke outbound tests, because a customer's CRM needs an address it can keep on a lead between runs. A permanent address fixes that, and the same address works whichever side writes first.

Routing stays simple because a Digital Human holds one open email conversation at a time. A reply to Maria's address can only belong to Maria's open thread, even if a help desk tool rewrites the subject line. To run more threads in parallel, a simulation adds more Digital Humans. We also set the standard reply headers on every email so the thread looks normal in the agent's mailbox.

Example: two conversations running at the same time on mail.bluejaysims.com. Arrows point from whoever writes first.

Digital Humans Lumen Mariamonitor+dh-104217 Support agent Danielmonitor+dh-104218 CRM Maria writes first Lumen writes first

Send through a queue

The app never sends mail directly. It writes a job for each turn and moves on. A separate worker sends the jobs.

Keep the raw message

Every reply is stored exactly as it arrived. That one choice drives most of the inbound design. The mail servers' verdicts are in the headers, so the delivery checks read facts instead of guessing.

1From: Lumen Support <[email protected]>2To: Maria Chen <[email protected]>3Subject: Re: My lamp never arrived4Authentication-Results: mx.mail.bluejaysims.com;5 spf=pass smtp.mailfrom=lumen.example;6 dkim=pass [email protected];7 dmarc=pass header.from=lumen.example8X-Spam-Status: No

The raw message also lets us read replies properly. Most arrive as a multipart message, a bundle of versions and attachments wrapped together.

1multipart/mixed2├─ multipart/alternative3│  ├─ text/plain          read this first4│  └─ text/html           used when there is no plain version5└─ application/pdf        receipt.pdf, skipped

We walk that structure and take the plain text version when there is one. When a reply only has HTML, we turn it into text and keep its paragraphs and sign off. Then we cut the quoted thread under the new text, so the agent never gets credit for words Maria wrote. Every stored message is keyed by its unique message ID, so a notification delivered twice never becomes two turns.

Trust nothing by default

The inbound path is open to the internet. Two checks protect it. First, every notification is signed, and we verify the signature against the mail service's own certificate before reading anything. Second, every reply has to pass one rule before it can touch a conversation, because anyone can type any From line.

Incoming messageResult
From Lumen's agent inbox or a listed reply address, and DMARC passesAccepted as the agent's next turn.
From Lumen's agent, and the domain publishes no DMARC policyWe check alignment ourselves. If the DKIM signature matches the From domain, it is accepted.
Claims to be Lumen's agent, and DMARC failsRejected. This is what a forged reply looks like.
From another authenticated address on Lumen's domainFlagged on the run with the fix, which is to add the address to the agent's reply addresses.
From anyone elseDropped. The conversation carries on.

Design for email time

A voice turn takes a second. An email turn can take an hour if a person approves replies. So each conversation has a quiet timer that resets on every reply. When it runs out, the conversation ends and gets graded.

A timer alone was not enough. If the first email never went out, no reply would ever arrive to start the timer, and the run would wait forever. So every conversation also gets a hard deadline when it starts, and a background sweep closes anything past it.

Grade each check the way it should be graded

Protect the domain

If our domain started landing in spam, every customer's tests would start failing because of us. Keeping it healthy is what keeps a failing test pointed at the agent.

What hurts a domainWhat we do
Bursts of mailThe send worker paces everything, and each simulation caps how many conversations are open at once.
Mailing addresses that bounceA hard bounce pauses the uptime monitor that caused it.
Spam complaintsA complaint pauses the monitor the same way.
Load testing on real inboxesVolume tests go to test addresses that accept mail without affecting reputation.

What we learned in production

Conclusion

Most email agent failures happen in delivery and authentication. We built email simulations to test that whole path. One shared domain keeps trust in one place, a paced queue keeps it healthy, raw messages give us the servers' own verdicts, and a strict trust rule makes sure every turn came from the agent. The Digital Human engine, grading and monitoring are the same ones that run voice and SMS, so a team can now test one agent on every channel its customers use, from the same place.

To try it, create an agent, choose Email, and enter the inbox your agent answers from. The email agent docs cover setup and the API.

Catch email failures
before customers do

Run Digital Humans against your email agent before launch and keep watching it in production.