<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Damian Dominella</title>
    <description>The latest articles on DEV Community by Damian Dominella (@damiandominella).</description>
    <link>https://dev.to/damiandominella</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F581738%2F7aa1d972-5861-4ed1-aaad-4a85222eeb51.jpeg</url>
      <title>DEV Community: Damian Dominella</title>
      <link>https://dev.to/damiandominella</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/damiandominella"/>
    <language>en</language>
    <item>
      <title>I built a tool that turns pasted email threads into shareable links: here's how</title>
      <dc:creator>Damian Dominella</dc:creator>
      <pubDate>Mon, 06 Jul 2026 20:32:34 +0000</pubDate>
      <link>https://dev.to/damiandominella/i-built-a-tool-that-turns-pasted-email-threads-into-shareable-links-heres-how-2clf</link>
      <guid>https://dev.to/damiandominella/i-built-a-tool-that-turns-pasted-email-threads-into-shareable-links-heres-how-2clf</guid>
      <description>&lt;p&gt;Last weekend I built &lt;a href="https://threadsnapp.com" rel="noopener noreferrer"&gt;Threadsnapp&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;The interesting part was the parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't use regex
&lt;/h2&gt;

&lt;p&gt;My first instinct was to parse email headers with regex. Sender line, timestamp line, recipient line, body. Should be simple enough.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;Regex would have been a 500-line monster with constant edge case patches. I would have been maintaining it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The storage abstraction trick
&lt;/h2&gt;

&lt;p&gt;I wanted to prototype the UX before wiring up a real database, so I wrapped all persistence in a &lt;code&gt;threadStore.ts&lt;/code&gt; abstraction with just two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;saveThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;getThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;threadStore.ts&lt;/code&gt;, nothing else changed.&lt;/p&gt;

&lt;p&gt;Clean separation that made the whole process much less stressful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I didn't expect
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;It's live at &lt;a href="https://threadsnapp.com" rel="noopener noreferrer"&gt;threadsnapp.com&lt;/a&gt;, free for now. Happy to answer questions about any part of the build.&lt;/p&gt;

</description>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
