<?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: Barbaros Emre Alagöz</title>
    <description>The latest articles on DEV Community by Barbaros Emre Alagöz (@barbarosalagoz).</description>
    <link>https://dev.to/barbarosalagoz</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%2F4206429%2Fb986d022-0e09-4930-b414-3262b79ad49c.jpg</url>
      <title>DEV Community: Barbaros Emre Alagöz</title>
      <link>https://dev.to/barbarosalagoz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/barbarosalagoz"/>
    <language>en</language>
    <item>
      <title>I was too lazy to read 53 emails, so a local LLM reads them for me</title>
      <dc:creator>Barbaros Emre Alagöz</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:17:20 +0000</pubDate>
      <link>https://dev.to/barbarosalagoz/i-built-an-ai-email-assistant-in-net-with-serpapi-and-a-local-llm-heres-what-broke-37od</link>
      <guid>https://dev.to/barbarosalagoz/i-built-an-ai-email-assistant-in-net-with-serpapi-and-a-local-llm-heres-what-broke-37od</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I had 53 unread emails and no idea which ones actually needed me. Newsletters, payment notices, receipts, and somewhere in between, a few real people waiting for an answer. I didn't want to read all of them. Honestly, I didn't want to read any of them.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;MailAssistant&lt;/strong&gt;, a small .NET console app that does the reading for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it skips newsletters and automated mail before any AI sees them,&lt;/li&gt;
&lt;li&gt;an open-weight model running on my own laptop sorts what's left, summarizes it, and drafts a reply,&lt;/li&gt;
&lt;li&gt;and for real senders, it looks up the company behind the email so I know who I'm talking to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two rules from day one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It never sends anything.&lt;/strong&gt; It drafts; I decide. Laziness has limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My email stays on my machine.&lt;/strong&gt; The model runs locally with Ollama. Only the sender's domain goes out to the internet, to SerpApi.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Who gets to be lazier? Anyone whose inbox is mostly noise with a few important emails hidden inside it. For me, that's every Monday morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;MailAssistant is a local console app, so there's no deployed link. Here's a run on a few test emails I sent to myself (the app works in Turkish):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanjm49floxmm6bruuvkq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanjm49floxmm6bruuvkq.png" alt=" " width="798" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run it yourself, you need the .NET SDK, Ollama with &lt;code&gt;qwen2.5:7b&lt;/code&gt;, a Gmail app password and a free SerpApi key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/barbarosalagoz/MailAssistant
&lt;span class="nb"&gt;cd &lt;/span&gt;MailAssistant
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"MAIL_USER"&lt;/span&gt; &lt;span class="s2"&gt;"you@gmail.com"&lt;/span&gt;
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"MAIL_APP_PASSWORD"&lt;/span&gt; &lt;span class="s2"&gt;"your-app-password"&lt;/span&gt;
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"SERPAPI_KEY"&lt;/span&gt; &lt;span class="s2"&gt;"your-serpapi-key"&lt;/span&gt;
dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/barbarosalagoz" rel="noopener noreferrer"&gt;
        barbarosalagoz
      &lt;/a&gt; / &lt;a href="https://github.com/barbarosalagoz/MailAssistant" rel="noopener noreferrer"&gt;
        MailAssistant
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AI email assistant in .NET: triages your inbox, drafts replies with a local LLM (Ollama), and researches the sender’s company with SerpApi. Human-in-the-loop, nothing sent automatically.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;MailAssistant&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;An AI email assistant in .NET that triages your inbox, drafts replies with a local LLM
and researches the sender's company with &lt;a href="https://serpapi.com" rel="nofollow noopener noreferrer"&gt;SerpApi&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Drafts are never sent automatically. A human always reviews them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Reads unread emails from the last 2 days over IMAP (read-only, nothing is marked or deleted).&lt;/li&gt;
&lt;li&gt;Skips newsletters and automated notifications (&lt;code&gt;List-Unsubscribe&lt;/code&gt; header and sender heuristics).&lt;/li&gt;
&lt;li&gt;Classifies each remaining email and drafts a reply with a local model via &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Looks up the sender's company with the official &lt;code&gt;serpapi&lt;/code&gt; .NET package and summarises it.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;.NET, C#&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jstedfast/MailKit" rel="noopener noreferrer"&gt;MailKit&lt;/a&gt; for IMAP&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/serpapi" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;serpapi&lt;/code&gt;&lt;/a&gt; .NET SDK (&lt;code&gt;google_light&lt;/code&gt; engine)&lt;/li&gt;
&lt;li&gt;Ollama with &lt;code&gt;qwen2.5:7b&lt;/code&gt; running locally: email content never leaves your machine for the LLM step&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Prerequisites: .NET SDK, Ollama (&lt;code&gt;ollama pull qwen2.5:7b&lt;/code&gt;), a Gmail app password, a SerpApi API key (free plan works).&lt;/p&gt;
&lt;p&gt;Store your secrets with .NET User Secrets…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/barbarosalagoz/MailAssistant" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I taught English for 17 years before I switched to software in 2023, and I'm still learning C# and .NET. If teaching taught me one thing, it's that you learn a tool fastest when you point it at a real problem. My inbox was a real problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gmail (IMAP, read-only)
   └─&amp;gt; filter newsletters and notifications (no AI)
         └─&amp;gt; qwen2.5:7b on Ollama: category + summary + draft reply
               └─&amp;gt; SerpApi: search the sender's company
                     └─&amp;gt; qwen2.5:7b: two-sentence company summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com/library/qwen2.5" rel="noopener noreferrer"&gt;Qwen2.5 7B&lt;/a&gt;&lt;/strong&gt;, an open-weight model, served locally by &lt;strong&gt;&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET Generic Host&lt;/strong&gt; with dependency injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jstedfast/MailKit" rel="noopener noreferrer"&gt;MailKit&lt;/a&gt;&lt;/strong&gt; for read-only IMAP&lt;/li&gt;
&lt;li&gt;the official &lt;strong&gt;&lt;a href="https://www.nuget.org/packages/serpapi" rel="noopener noreferrer"&gt;&lt;code&gt;serpapi&lt;/code&gt;&lt;/a&gt; .NET package&lt;/strong&gt; for company research&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The laziest part is the part with no AI.&lt;/strong&gt; My first version sent everything to the model. It wrote polite replies to no-reply addresses, and I burned SerpApi searches looking up the companies behind newsletters. So now a plain filter runs first. Bulk mail usually carries a &lt;code&gt;List-Unsubscribe&lt;/code&gt; header, and automated senders follow predictable patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsBulkOrAutomated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MimeMessage&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"List-Unsubscribe"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mailboxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;localMarkers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"noreply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"no-reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed-payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"billing"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;hostLabels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"trx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"news"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mailer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bounce"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;localMarkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hostLabels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the filter was in, most of the inbox never reached the model. That's when it clicked for me: the AI was the smaller part of the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Talking to the local model&lt;/strong&gt; is one HTTP call. Ollama's &lt;code&gt;/api/chat&lt;/code&gt; endpoint takes a system prompt, the email, and &lt;code&gt;format: "json"&lt;/code&gt;, so I get back a category, a summary and a draft that deserialize straight into a C# record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;DraftReply&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Company research with SerpApi.&lt;/strong&gt; The &lt;code&gt;serpapi&lt;/code&gt; package registers its client through &lt;code&gt;IHttpClientFactory&lt;/code&gt;, so it fits the Generic Host like any typed client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSerpApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"SERPAPI_KEY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SERPAPI_KEY is not set."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each real sender, I search the company's base domain with the &lt;code&gt;google_light&lt;/code&gt; engine, pass the top five titles and snippets to the local model, and ask for a two-sentence summary. The package's typed exceptions (&lt;code&gt;SerpApiKeyException&lt;/code&gt;, &lt;code&gt;SerpApiHttpException&lt;/code&gt;, &lt;code&gt;SerpApiTimeoutException&lt;/code&gt;) mean a bad key or an empty quota shows up as a readable message instead of a stack trace. And the Account API tells me how many searches I have left without using one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup, before any AI ran.&lt;/strong&gt; My first &lt;code&gt;dotnet new&lt;/code&gt; failed because the terminal had opened in &lt;code&gt;C:\Windows\System32&lt;/code&gt;. Then I called Ollama over &lt;code&gt;https&lt;/code&gt; and got a cryptic SSL "frame size" error; locally it speaks plain &lt;code&gt;http&lt;/code&gt;. Then my Google Workspace account only allowed passkeys, so I tested with a personal Gmail account and an app password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model invented a phishing scam.&lt;/strong&gt; A Disney+ payment email came from &lt;code&gt;trx.mail2.disneyplus.com&lt;/code&gt;. I searched that exact subdomain, got almost nothing back, and the model decided Disney+ was a phishing operation. Fix: search the base domain, and tell the model that if the search results don't say it, it shouldn't either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halfway through a summary, it switched to Chinese.&lt;/strong&gt; One summary started in Turkish and ended in Chinese. An explicit "Turkish only" rule fixed it. Small local models need very explicit rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model copied a secret link into a draft.&lt;/strong&gt; A failed-payment email contained a billing link with a secret token, and the model helpfully pasted it into the reply. Telling it "no links" wasn't enough, so the code now strips every URL from drafts. The prompt asks; the code enforces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one scared me the most. The phishing claim was obviously wrong. The leaked link looked like a perfectly helpful reply, and that's exactly what made it dangerous. If the app had been allowed to send emails, it would have gone out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security, briefly.&lt;/strong&gt; Email is untrusted input, so I mapped the risks to the &lt;a href="https://genai.owasp.org/llm-top-10/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications&lt;/a&gt;: email content goes inside &lt;code&gt;&amp;lt;email&amp;gt;&lt;/code&gt; tags and is treated as data (prompt injection), the model runs locally (sensitive data), the mailbox is read-only and nothing is sent automatically (excessive agency), and company summaries may only use what the search results say (misinformation).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because this project reads my email. All of it: invoices, contracts, the occasional message from my mother.&lt;/p&gt;

&lt;p&gt;With a closed API, every one of those emails would leave my laptop and land on someone else's server, just so a model could tell me "this is a newsletter." With an open-weight model running on Ollama, the inbox never leaves my machine. The only thing that goes out is a company domain, which is public anyway.&lt;/p&gt;

&lt;p&gt;Open weights gave me three more things a closed API wouldn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I can see where it fails and fix it myself.&lt;/strong&gt; When the model drifted into Chinese or invented a phishing scam, I could test the same prompt again and again for free, change the rules, and run it again. No per-token bill for my mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap the model.&lt;/strong&gt; If a better small model comes out next month, changing it is one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs on a normal laptop.&lt;/strong&gt; No GPU cluster, no account, no credit card. Anyone who clones the repo can run the whole thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And open source goes beyond the model. MailKit, Ollama and the SerpApi .NET SDK are all open source. When I needed to know exactly how &lt;code&gt;AddSerpApi&lt;/code&gt; registers its client, I read the SDK's source on GitHub instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;I used it on my own inbox this week. Instead of opening 53 emails, I read a short list: the few emails that needed a human, each with a summary, a draft reply, and a two-line note about who the sender is. Everything else was filtered out without me touching it.&lt;/p&gt;

&lt;p&gt;The drafts aren't perfect, and that's fine. Editing a draft is much lazier than writing a reply from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi&lt;/strong&gt;: company research for each real sender, using the official &lt;code&gt;serpapi&lt;/code&gt; .NET package (&lt;code&gt;AddSerpApi&lt;/code&gt;, &lt;code&gt;google_light&lt;/code&gt;, Account API, typed exceptions).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I wrote this project and this post with help from an AI assistant. It suggested; I ran, broke, fixed and decided.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
