DEV Community

Cover image for I was too lazy to read 53 emails, so a local LLM reads them for me
Barbaros Emre Alagöz
Barbaros Emre Alagöz

Posted on

I was too lazy to read 53 emails, so a local LLM reads them for me

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

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.

So I built MailAssistant, a small .NET console app that does the reading for me:

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

Two rules from day one:

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

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.

Demo

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):

To run it yourself, you need the .NET SDK, Ollama with qwen2.5:7b, a Gmail app password and a free SerpApi key:

git clone https://github.com/barbarosalagoz/MailAssistant
cd MailAssistant
dotnet user-secrets set "MAIL_USER" "you@gmail.com"
dotnet user-secrets set "MAIL_APP_PASSWORD" "your-app-password"
dotnet user-secrets set "SERPAPI_KEY" "your-serpapi-key"
dotnet run
Enter fullscreen mode Exit fullscreen mode

Code

GitHub logo barbarosalagoz / MailAssistant

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.

MailAssistant

An AI email assistant in .NET that triages your inbox, drafts replies with a local LLM and researches the sender's company with SerpApi.

Drafts are never sent automatically. A human always reviews them.

What it does

  1. Reads unread emails from the last 2 days over IMAP (read-only, nothing is marked or deleted).
  2. Skips newsletters and automated notifications (List-Unsubscribe header and sender heuristics).
  3. Classifies each remaining email and drafts a reply with a local model via Ollama.
  4. Looks up the sender's company with the official serpapi .NET package and summarises it.

Tech

  • .NET, C#
  • MailKit for IMAP
  • serpapi .NET SDK (google_light engine)
  • Ollama with qwen2.5:7b running locally: email content never leaves your machine for the LLM step

Run it

Prerequisites: .NET SDK, Ollama (ollama pull qwen2.5:7b), a Gmail app password, a SerpApi API key (free plan works).

Store your secrets with .NET User Secrets…

How I Built It

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.

The pipeline:

Gmail (IMAP, read-only)
   └─> filter newsletters and notifications (no AI)
         └─> qwen2.5:7b on Ollama: category + summary + draft reply
               └─> SerpApi: search the sender's company
                     └─> qwen2.5:7b: two-sentence company summary
Enter fullscreen mode Exit fullscreen mode

The stack:

  • Qwen2.5 7B, an open-weight model, served locally by Ollama
  • .NET Generic Host with dependency injection
  • MailKit for read-only IMAP
  • the official serpapi .NET package for company research

The laziest part is the part with no AI. 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 List-Unsubscribe header, and automated senders follow predictable patterns:

static bool IsBulkOrAutomated(MimeMessage m)
{
    if (m.Headers.Contains("List-Unsubscribe")) return true;

    var address = m.From.Mailboxes.FirstOrDefault()?.Address?.ToLowerInvariant() ?? "";
    var parts = address.Split('@');
    var local = parts[0];
    var host = parts.Length > 1 ? parts[1] : "";

    string[] localMarkers = { "noreply", "no-reply", "notifications", "failed-payments", "billing" };
    string[] hostLabels = { "mail", "trx", "info", "news", "mailer", "bounce" };

    return localMarkers.Any(local.Contains)
        || host.Split('.').Any(label => hostLabels.Contains(label));
}
Enter fullscreen mode Exit fullscreen mode

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.

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

public sealed record Triage(string Category, string Summary, string DraftReply);
Enter fullscreen mode Exit fullscreen mode

Company research with SerpApi. The serpapi package registers its client through IHttpClientFactory, so it fits the Generic Host like any typed client:

builder.Services.AddSerpApi(options =>
{
    options.ApiKey = builder.Configuration["SERPAPI_KEY"]
        ?? throw new InvalidOperationException("SERPAPI_KEY is not set.");
    options.Timeout = TimeSpan.FromSeconds(30);
});
Enter fullscreen mode Exit fullscreen mode

For each real sender, I search the company's base domain with the google_light engine, pass the top five titles and snippets to the local model, and ask for a two-sentence summary. The package's typed exceptions (SerpApiKeyException, SerpApiHttpException, SerpApiTimeoutException) 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.

What broke along the way:

  • The setup, before any AI ran. My first dotnet new failed because the terminal had opened in C:\Windows\System32. Then I called Ollama over https and got a cryptic SSL "frame size" error; locally it speaks plain http. Then my Google Workspace account only allowed passkeys, so I tested with a personal Gmail account and an app password.
  • The model invented a phishing scam. A Disney+ payment email came from trx.mail2.disneyplus.com. 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.
  • Halfway through a summary, it switched to Chinese. One summary started in Turkish and ended in Chinese. An explicit "Turkish only" rule fixed it. Small local models need very explicit rules.
  • The model copied a secret link into a draft. 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.

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.

Security, briefly. Email is untrusted input, so I mapped the risks to the OWASP Top 10 for LLM Applications: email content goes inside <email> 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).

Why Does Open Innovation Matter?

Because this project reads my email. All of it: invoices, contracts, the occasional message from my mother.

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.

Open weights gave me three more things a closed API wouldn't:

  • I can see where it fails and fix it myself. 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.
  • I can swap the model. If a better small model comes out next month, changing it is one line.
  • It runs on a normal laptop. No GPU cluster, no account, no credit card. Anyone who clones the repo can run the whole thing.

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 AddSerpApi registers its client, I read the SDK's source on GitHub instead of guessing.

The Lazy Payoff

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.

The drafts aren't perfect, and that's fine. Editing a draft is much lazier than writing a reply from zero.

Prize Categories

  • SerpApi: company research for each real sender, using the official serpapi .NET package (AddSerpApi, google_light, Account API, typed exceptions).

I wrote this project and this post with help from an AI assistant. It suggested; I ran, broke, fixed and decided.

Top comments (0)