Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions server/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,47 @@ export default {
if (!ok) console.warn('[cron:discovery] heartbeat failed');
}));
},

async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {
const from = message.from;
const to = message.to;
const subject = message.headers.get('subject') || '(no subject)';
const messageId = message.headers.get('message-id') || `${Date.now()}`;
const size = message.rawSize;

console.log(`[email:inbound] from=${from} to=${to} subject="${subject}" size=${size}`);

// Store raw email in R2 for document ingestion pipeline
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const sanitizedId = messageId.replace(/[<>]/g, '').replace(/[^a-zA-Z0-9@._-]/g, '_');
const key = `inbound-email/${ts}_${sanitizedId}.eml`;

const rawBytes = await new Response(message.raw).arrayBuffer();
await env.FINANCE_R2.put(key, rawBytes, {
Comment on lines +39 to +40
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Response(message.raw).arrayBuffer() buffers the entire email into memory before writing to R2. For larger messages this can be slow and risk Worker memory limits; prefer streaming message.raw directly to R2Bucket.put (or otherwise avoid holding the full payload in memory) since message.rawSize is already available for sizing.

Copilot uses AI. Check for mistakes.
customMetadata: {
from,
to,
subject,
messageId,
receivedAt: new Date().toISOString(),
sizeBytes: String(size),
Comment on lines +45 to +47
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receivedAt is generated multiple times (R2 metadata and KV index) independently of the ts used in the object key. This can lead to inconsistent timestamps for the same email and makes correlation/debugging harder. Consider capturing a single receivedAt once and reusing it for ts/R2 metadata/KV entry.

Copilot uses AI. Check for mistakes.
},
});

console.log(`[email:inbound] stored in R2: ${key} (${rawBytes.byteLength} bytes)`);

// Index in KV for quick lookup
const kv = env.FINANCE_KV;
const indexEntry = JSON.stringify({
key,
from,
to,
subject,
receivedAt: new Date().toISOString(),
sizeBytes: rawBytes.byteLength,
});
await kv.put(`email:inbound:${ts}`, indexEntry, { expirationTtl: 86400 * 90 }); // 90 days
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make KV index keys collision-resistant

Using only ts in the KV key means two emails processed within the same millisecond will write to the same key and the later write overwrites the earlier index record. This drops one message from the 90-day lookup index even though both .eml files may exist in R2, which is especially likely under bursty inbound traffic or multi-region concurrent handling. Include a stable per-message discriminator (for example sanitized message-id or a random suffix) in the KV key to avoid silent index loss.

Useful? React with 👍 / 👎.

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KV index key uses only the timestamp (email:inbound:${ts}), so two emails processed within the same millisecond will overwrite each other and you’ll lose index entries. Also, the PR description mentions indexing by <ts>_<msgid>, but the KV key currently doesn’t include the message id. Use a collision-resistant key (e.g., include sanitizedId/messageId and/or a random suffix like crypto.randomUUID()).

Suggested change
await kv.put(`email:inbound:${ts}`, indexEntry, { expirationTtl: 86400 * 90 }); // 90 days
const indexKey = `email:inbound:${ts}_${sanitizedId}_${crypto.randomUUID()}`;
await kv.put(indexKey, indexEntry, { expirationTtl: 86400 * 90 }); // 90 days

Copilot uses AI. Check for mistakes.
},
} satisfies ExportedHandler<Env>;

// Re-export the Agent DO class so Wrangler can bind it
Expand Down
Loading