When a visitor sends a message and the agent isn't online, we email them: subject [muro] new message from Léa on lumiere.shop, with the message body, attachments, and a hidden header that pins the conversation ID. The agent replies from Gmail. The visitor sees a normal chat reply seconds later. There is no agent app open, no portal login.
Outbound, the easy direction
Outbound is mostly about deliverability. We sign all outbound with DKIM, set up SPF, configure DMARC. We send from [email protected] via AWS SES (EU region), with a one-click List-Unsubscribe header so Gmail and Outlook surface the unsubscribe button natively.
textSubject: [muro] Sara → "Do you ship to Spain?"
From: [email protected]
Reply-To: reply+conv_a8b3@<inbound domain>
List-Unsubscribe: <https://muro.chat/api/unsubscribe?token=…>
References: <[email protected]>
Hi Léa, Sara just messaged on lumiere.shop:
> Do you ship to Spain?
[Open in muro] · [Reply by email below]Inbound, the hard direction
Inbound is where the engineering lives. SES drops the raw mail in S3, fires our Lambda parser, and we get to the messy part.
1, Strip the quoted reply
Gmail wraps the original message in <blockquote class="gmail_quote">. Outlook uses <div type="cite">. Apple Mail uses > plain-text quoting. We have a parser per client. Mistakes here are visible: a missed strip means the visitor sees the agent's reply followed by their own original message echoed back.
2, Strip signatures
Two algorithms run in parallel: a -- \n separator detector (RFC 3676) and a heuristic that looks for "Sent from my iPhone", phone numbers, repeated lines, etc. We err toward keeping content rather than deleting too aggressively.
3, Reconcile to a thread
We use three signals in priority order: the Reply-To token (most reliable), the In-Reply-To/References headers (RFC 2822), and as last resort a fingerprint match on the workspace + agent email + recent conversation. If two signals disagree we route to a "needs review" queue rather than guess wrong.
The bouncer
The single most useful piece of inbound code we wrote is the autoresponder detector. Out-of-office replies, bounce messages, mailing-list digests, none of these should become chat messages. We check Auto-Submitted, X-Autoreply, Precedence, and a body classifier. False positives here are silent (the visitor never sees a noise message). False negatives are loud and embarrassing.
Things we considered and rejected
- →Polling IMAP: works, but ~30s latency and breaks on Gmail OAuth scope changes
- →Per-customer SMTP: too much surface, customers don't want to host their own MX
- →A second SaaS for inbound: fine, but the SES integration is simpler at our scale
- →Markdown in email replies: tempting, but most agents type plain text in Gmail. We render visitor-side as plain text + automatic URL detection.
Forwarding is the feature that quietly does most of the work in this product. Every plan includes it. There is no "email forwarding add-on", that would be a tax on something already nearly free to deliver.
