Sending & Receiving
How email flows through daimon.email infrastructure
Overview
daimon.email handles both inbound (receiving) and outbound (sending) email for AI agents. Unlike traditional email providers that require OAuth or domain verification, daimon.email provides instant email addresses via API — no human verification required.
This guide explains how email flows through the system in both directions.
Architecture Overview
graph LR
A[Internet] -->|SMTP| B[Cloudflare Email Worker]
B -->|Parse & Store| C[API Worker]
C -->|Write| D[(Supabase)]
C -->|Store MIME| E[R2 Storage]
C -->|Notify| F[Your Agent]
G[Your Agent] -->|POST /send| C
C -->|Queue| H[Inngest]
H -->|SMTP Delivery| I[Recipient]
style B fill:#f39c12
style C fill:#3498db
style D fill:#27ae60
style E fill:#9b59b6Key Components
- Inbound Worker - Cloudflare Email Worker receives all
*@daimon.emailmail - API Worker - Hono.js REST API on Cloudflare Workers (
api.daimon.email) - Supabase - PostgreSQL database for messages, threads, accounts
- R2 Storage - Cloudflare object storage for raw MIME and attachments
- Inngest - Background job queue for outbound delivery (Sprint 3)
Receiving Email (Inbound Flow)
When someone sends email to an address like agent-abc123@daimon.email:
Step 1: Email Arrives
External mail server delivers to daimon.email MX records, which point to Cloudflare's email infrastructure:
daimon.email. MX 10 amir.mx.cloudflare.net.
daimon.email. MX 10 linda.mx.cloudflare.net.
daimon.email. MX 10 isaac.mx.cloudflare.net.Cloudflare's Email Worker (configured with catch-all *@daimon.email) receives the message.
Step 2: Parse & Extract
The inbound worker:
- Parses the raw MIME email using
postal-mimelibrary - Extracts headers (
From,To,Subject,Message-ID,In-Reply-To,References) - Extracts body (text, HTML, attachments)
- Detects threading signals (
In-Reply-To,References) - Extracts reply text using TalonJS (strips quoted history)
- Extracts links and identifies CTA links (verification URLs, etc.)
Step 3: Store & Index
The API worker stores the parsed message:
-- Insert message
INSERT INTO messages (
inbox_id,
thread_id,
message_id_header,
from_email,
to_email,
subject,
text_body,
html_body,
reply_body,
links,
cta_links,
received_at
) VALUES (...);
-- Store raw MIME in R2
PUT /emails/{inbox_id}/{message_id}.eml
-- Store attachments in R2
PUT /attachments/{message_id}/{filename}The worker determines which inbox owns this email by parsing the recipient address:
agent-abc123@daimon.email→ inbox with addressagent-abc123support@custom-domain.com→ inbox with verified custom domain (paid tier)
Step 4: Thread Matching
The system automatically groups related emails into threads:
- Check if
In-Reply-Toheader matches existingmessage_id_headerin same inbox - If match found, add message to that thread
- If no match, check
Referencesheader for any matching message IDs - If still no match, create new thread with subject as thread name
// Simplified threading logic
function findOrCreateThread(message: ParsedEmail, inboxId: string) {
// Try In-Reply-To first
if (message.headers['in-reply-to']) {
const parent = findMessage({
inbox_id: inboxId,
message_id_header: message.headers['in-reply-to'],
});
if (parent) {
return parent.thread_id;
}
}
// Try References
if (message.headers['references']) {
const refIds = message.headers['references'].split(/\s+/);
for (const refId of refIds.reverse()) {
const parent = findMessage({
inbox_id: inboxId,
message_id_header: refId,
});
if (parent) {
return parent.thread_id;
}
}
}
// Create new thread
return createThread({
inbox_id: inboxId,
subject: message.subject,
});
}Step 5: Notify Your Agent
If your agent registered a webhook, daimon.email sends a message.received event:
{
"type": "message.received",
"timestamp": "2026-03-16T14:23:45Z",
"data": {
"message_id": "msg_9kL2xYw1pQm3",
"inbox_id": "inbox_abc123",
"thread_id": "thread_xyz789",
"from": "customer@company.com",
"to": ["agent-abc123@daimon.email"],
"subject": "Question about pricing",
"text_body": "Hi, I'm interested in your product...",
"reply_body": "Hi, I'm interested in your product...",
"links": ["https://yoursite.com/pricing"],
"cta_links": [],
"received_at": "2026-03-16T14:23:45Z"
}
}app.post('/webhooks/daimon', async (req, res) => {
const event = req.body;
if (event.type === 'message.received') {
const message = event.data;
// Agent processes the email
const response = await generateResponse(message.text_body);
// Agent sends reply
await daimonClient.inboxes.send(message.inbox_id, {
to: message.from,
subject: `Re: ${message.subject}`,
text: response,
thread_id: message.thread_id, // Keep in same thread
});
}
res.json({ received: true });
});Polling (Alternative to Webhooks)
If you don't use webhooks, poll for new messages:
// Check for new messages every 30 seconds
setInterval(async () => {
const messages = await client.inboxes.listMessages(inboxId, {
received_after: lastCheckTime.toISOString(),
limit: 100,
});
for (const message of messages.messages) {
await handleInboundMessage(message);
}
lastCheckTime = new Date();
}, 30000);import time
from datetime import datetime
last_check_time = datetime.now()
while True:
# Poll for new messages
messages = client.inboxes.list_messages(
inbox_id=inbox_id,
received_after=last_check_time.isoformat(),
limit=100
)
for message in messages.messages:
handle_inbound_message(message)
last_check_time = datetime.now()
time.sleep(30) # Poll every 30 secondsSending Email (Outbound Flow)
When your agent sends email via POST /v1/inboxes/{id}/send:
Step 1: API Request
const sent = await client.inboxes.send(inboxId, {
to: 'recipient@company.com',
subject: 'Your support ticket is resolved',
text: 'Hi, I found a solution to your issue...',
html: '<p>Hi, I found a solution to your issue...</p>',
attachments: [
{
filename: 'solution.pdf',
content_type: 'application/pdf',
data: base64EncodedPdf,
},
],
thread_id: 'thread_xyz789', // Optional: keep in thread
send_at: '2026-03-17T09:00:00Z', // Optional: schedule for later
});
console.log(sent.message_id); // "msg_kLp9xYw2qRn4"
console.log(sent.status); // "queued" or "scheduled"sent = client.inboxes.send(
inbox_id=inbox_id,
to='recipient@company.com',
subject='Your support ticket is resolved',
text='Hi, I found a solution to your issue...',
html='<p>Hi, I found a solution to your issue...</p>',
attachments=[{
'filename': 'solution.pdf',
'content_type': 'application/pdf',
'data': base64_encoded_pdf
}],
thread_id='thread_xyz789',
send_at='2026-03-17T09:00:00Z'
)
print(sent.message_id) # "msg_kLp9xYw2qRn4"
print(sent.status) # "queued" or "scheduled"curl -X POST https://api.daimon.email/v1/inboxes/inbox_abc123/send \
-H "Authorization: Bearer dm_live_..." \
-H "Content-Type: application/json" \
-d '{
"to": "recipient@company.com",
"subject": "Your support ticket is resolved",
"text": "Hi, I found a solution to your issue...",
"html": "<p>Hi, I found a solution to your issue...</p>",
"thread_id": "thread_xyz789"
}'Step 2: Validation & Queuing
The API worker:
- Validates your account has send capability (free tier blocked)
- Checks daily send rate limits
- Creates message record in
messagestable with statusqueued - Enqueues background job in Inngest for SMTP delivery
// Simplified send flow
async function sendEmail(inbox: Inbox, payload: SendRequest) {
// Check capability
const account = await getAccount(inbox.account_id);
if (account.tier === 'free') {
throw new Error402({
error: 'SEND_REQUIRES_PAID',
upgrade_context: {
operator_action_url: `https://daimon.email/upgrade?token=${createUpgradeToken(account)}`,
operator_action_label: 'Add payment method to enable sending',
agent_script: 'Tell your operator: I need sending access. Here is a direct upgrade link: {url}',
},
});
}
// Check rate limit
if (account.sends_today >= account.daily_send_limit) {
throw new Error429({ error: 'SEND_LIMIT_EXCEEDED' });
}
// Create message
const message = await createMessage({
inbox_id: inbox.id,
to: payload.to,
subject: payload.subject,
text_body: payload.text,
html_body: payload.html,
status: payload.send_at ? 'scheduled' : 'queued',
send_at: payload.send_at,
});
// Queue for delivery (unless scheduled)
if (!payload.send_at) {
await inngest.send({
name: 'email.send',
data: { message_id: message.id },
});
}
return message;
}Step 3: SMTP Delivery (Currently Stubbed)
Note
Sprint 1 Status: Outbound SMTP delivery is stubbed. Messages return status: "queued" but are not yet delivered. Real delivery will be implemented when GREEN_ARROW_SMTP_SERVER env var is configured (Sprint 3).
When fully implemented, the Inngest background worker will:
- Construct RFC 5322-compliant MIME message
- Connect to outbound SMTP relay (Green Arrow or AWS SES)
- Send via SMTP with proper SPF/DKIM signing
- Update message status to
senton success - Handle bounces/rejections and update status to
bouncedorfailed - Fire
message.sentwebhook event
Step 4: Reply & Forward Auto-Threading
When sending a reply or forward, include thread_id to maintain conversation context:
// Inbound message arrives
const inbound = await client.inboxes.getMessage(inboxId, 'msg_abc123');
// Send reply in same thread
const reply = await client.inboxes.send(inboxId, {
to: inbound.from,
subject: `Re: ${inbound.subject}`,
text: 'Thanks for your email. Here is the answer...',
thread_id: inbound.thread_id, // Auto-threading
in_reply_to: inbound.message_id_header, // Sets In-Reply-To header
});
// Message inherits thread context
console.log(reply.thread_id === inbound.thread_id); // trueThe API automatically sets proper email headers:
In-Reply-To: <original-message-id>References: <all-prior-message-ids-in-thread>
This ensures Gmail, Outlook, and other clients display messages as conversations.
Message States
Messages transition through these states:
stateDiagram-v2
[*] --> received: Inbound
[*] --> queued: Outbound (immediate)
[*] --> scheduled: Outbound (delayed)
scheduled --> queued: send_at reached
queued --> sending: SMTP connection
sending --> sent: Delivery confirmed
sending --> failed: SMTP error
sending --> bounced: Recipient rejected
sent --> [*]
failed --> [*]
bounced --> [*]
received --> [*]| Status | Description | Inbound/Outbound |
|---|---|---|
received | Successfully received from sender | Inbound |
queued | Waiting for SMTP delivery | Outbound |
scheduled | Waiting for send_at time | Outbound |
sending | SMTP delivery in progress | Outbound |
sent | Successfully delivered | Outbound |
failed | Delivery failed (permanent) | Outbound |
bounced | Recipient rejected (invalid address) | Outbound |
Rate Limits
| Tier | Daily Send Limit | Receive Limit |
|---|---|---|
| Free | 0 (send disabled) | Unlimited |
| Starter | 1,000/day | Unlimited |
| Pro | 10,000/day | Unlimited |
| Enterprise | Custom | Unlimited |
Info
Receive limits are per-inbox, not per-account. If you need high-volume inbound (>10k emails/day to one inbox), contact enterprise sales.
Webhooks for Send Status
Subscribe to outbound delivery events:
await client.webhooks.create({
url: 'https://your-agent.com/webhooks/daimon',
events: [
'message.queued',
'message.sent',
'message.failed',
'message.bounced',
],
});Event payloads:
{
"type": "message.sent",
"timestamp": "2026-03-16T14:30:12Z",
"data": {
"message_id": "msg_kLp9xYw2qRn4",
"inbox_id": "inbox_abc123",
"to": ["recipient@company.com"],
"subject": "Your support ticket is resolved",
"smtp_message_id": "<abc123@daimon.email>",
"delivered_at": "2026-03-16T14:30:12Z"
}
}{
"type": "message.bounced",
"timestamp": "2026-03-16T14:30:15Z",
"data": {
"message_id": "msg_kLp9xYw2qRn4",
"inbox_id": "inbox_abc123",
"to": ["invalid@nonexistent-domain.com"],
"bounce_type": "permanent",
"bounce_reason": "550 5.1.1 Recipient address rejected: User unknown",
"smtp_code": 550
}
}Best Practices
Use Webhooks for Inbound
Webhooks are more reliable than polling and reduce latency. Polling should be a fallback.
Handle Bounces Gracefully
Subscribe to message.bounced events and remove invalid addresses from your lists.
Include thread_id for Replies
Always set thread_id when replying to keep conversations organized.
Monitor Send Rate Limits
Track your daily send count via GET /v1/account to avoid hitting limits.
Next Steps
- Reply Extraction - How TalonJS extracts clean reply text
- Deliverability - Ensure your emails reach inboxes
- IMAP & SMTP - Traditional protocol access (paid tiers)