DEV Community

Damian Dominella
Damian Dominella

Posted on

I built a tool that turns pasted email threads into shareable links: here's how

Last weekend I built Threadsnapp, a small tool that takes raw email text (copy-pasted from Gmail, Outlook, Apple Mail) and turns it into a clean, public, shareable URL. No login, no extension, no mailbox access needed.

The idea came from a simple search: I needed something like this, asked around, searched Product Hunt and Indie Hackers, found nothing. So I built it.

The stack is straightforward: Next.js App Router, Neon for the database (Postgres, simple SQL), Tailwind + shadcn/ui. Nothing exotic.

The interesting part was the parser.

Why I didn't use regex

My first instinct was to parse email headers with regex. Sender line, timestamp line, recipient line, body. Should be simple enough.

Then I looked at real copy-pasted email text across different clients and languages. The input space is genuinely messy: different clients format headers differently, relative dates in multiple languages, attachment indicator lines that need to be stripped, "To: me" expressed in the local language of the email client, Outlook-style quoted reply blocks in both Italian and English with completely different key names (Da/Inviato/A/Oggetto vs From/Sent/To/Subject).

Regex would have been a 500-line monster with constant edge case patches. I would have been maintaining it forever.

What I did instead

I offloaded the structural extraction to an LLM (Groq, Llama 3.3 70B, free tier). The system prompt instructs it to return a clean JSON array of message objects: senderName, senderEmail, timestamp, to, cc, and body. Quoted reply blocks get stripped since they duplicate already-known info.

The parser lives in a single Next.js API route. The client sends the raw text, gets back the structured array, renders the preview. I debounce the call 600ms after the user stops typing so it doesn't hammer the API on every keystroke.

The fallback is simple: if the response isn't valid JSON or returns fewer than 2 messages, render the entire raw text as a single unstyled block. Never crash.

The storage abstraction trick

I wanted to prototype the UX before wiring up a real database, so I wrapped all persistence in a threadStore.ts abstraction with just two functions:

saveThread(rawText: string, expiresAt: string | null): Promise<{ slug: string }>
getThread(slug: string): Promise<{ rawText: string, expiresAt: string | null } | null>
Enter fullscreen mode Exit fullscreen mode

The prototype implementation base64-encoded the thread data directly into the slug, so the "URL" was essentially self-contained with zero backend. When I was happy with the UX I swapped in Neon (Postgres), rewrote only threadStore.ts, nothing else changed.

Clean separation that made the whole process much less stressful.

The thing I didn't expect

Halfway through building I realized: this isn't really an email tool. It's a "paste any conversation, get a shareable link" tool.

The parser already handles arbitrary structured text. The UI doesn't care if the thread came from Gmail or from a Slack export or a WhatsApp chat. The problem being solved, "I have a conversation I want to share with someone outside the context where it happened", is the same regardless of the source app.

So I expanded the scope a bit. Threadsnapp now handles email threads but the architecture is ready to support other formats. Probably where I'll take it next.

What I'd do differently

The live preview being async (LLM call) adds a small loading delay that a local parser wouldn't have. For a next iteration I'd try a fast local parse first as a best-effort attempt, fall back to the LLM only when confidence is low (less than 2 detected messages). Best of both worlds.


It's live at threadsnapp.com, free for now. Happy to answer questions about any part of the build.

Top comments (0)