Skip to content

About

An AI mail client where the assistant drives the UI — compose, search, open and reply by natural language, against real Gmail with a self-hosted Ollama model.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AI Mail Copilot

An AI-powered mail client where the assistant controls the UI — it composes and sends email, searches and filters your inbox, opens messages, and replies — all driven by natural language. Not a chatbot: when you say "send an email to John," the compose form opens and visibly fills in; when you say "show unread from this week," the inbox list updates.

Built with Next.js + TypeScript, CopilotKit for the agent↔UI layer, the Gmail API for real mail, and a self-hosted Ollama model as the LLM.

⚠️ Runs entirely locally against real Gmail. No data leaves your machine except calls to Google (Gmail) and your own Ollama host.


Demo

▶️ Watch the demo

A short screen recording of the assistant driving the UI. It walks through:

  1. Compose & send via natural language — "Send an email to … with subject … and body …" → the compose form opens, fields visibly fill in, and a confirmation card gates the send.
  2. Search updates the main UI — "Show me unread emails from this week" → the inbox list itself repopulates (plus email-card previews in the chat).
  3. Navigate & open — "Open the latest email from …" → that message opens in the reading pane.
  4. Context awareness — while reading an email, "reply to this" → a reply is pre-filled with the right recipient and quoted text.
  5. Real-time sync — a new email lands in the inbox with no manual refresh.

Features

Area What it does
Mail client Inbox & Sent lists, email detail (sandboxed HTML render), compose & send — all against real Gmail
Assistant — compose "Send an email to john@acme.com about lunch" → opens compose, types the fields in, asks you to confirm, then sends
Assistant — search/filter "Show unread from this week" / "emails from Sarah about the update" → the main list updates; matches also render as cards in the chat
Assistant — navigate/open "Open the latest email from David" → opens that message in the reading pane
Context awareness The assistant knows the open email, current folder, compose draft, and visible list — so "reply to this" just works
Filters Sender, keyword, unread, date range, relative windows — via UI controls and natural language
Real-time sync New mail appears in the inbox with no manual refresh (SSE + Gmail History polling)
Extras Reply/forward, human-in-the-loop send confirmation, rich email-card previews in chat, thread/conversation view, dark mode, unit tests

Architecture

Clean separation between the three concerns this app has to keep apart — mail service, UI, and AI — inside one Next.js app.

Browser (React)                         Next.js server (route handlers)
┌───────────────────────────┐          ┌──────────────────────────────┐
│ CopilotSidebar (chat)     │          │ /api/auth/*   Google OAuth   │
│ useMailCopilot (actions + │  HTTP/   │ /api/mail/*   Gmail REST     │
│   readable context) ──────┼─ SSE ──► │ /api/sync/stream  SSE sync   │
│ Zustand store  ◄──────────┤          │ /api/copilotkit   LLM runtime│
│ Mail UI (list/detail/…)   │          └───────┬───────────┬──────────┘
└───────────────────────────┘                  │           │
                                        lib/mail (Gmail)  Ollama (/v1)
  • Mail service — src/lib/mail/*: a provider-agnostic domain model with a Gmail service (gmail-service.ts), a structured-filter → Gmail-query compiler (query.ts), and a message parser (parse.ts). Nothing above this layer knows Gmail's raw message shape.
  • UI — src/components/* render from a single Zustand store (src/store/appStore.ts), which is the one source of truth for view, selection, compose draft, filters, and results.
  • AI — src/hooks/useMailCopilot.tsx registers CopilotKit actions (each handler mutates the store, so the UI visibly reacts) and readables (the live UI state the model can see). The /api/copilotkit runtime routes the LLM to Ollama.

Why the assistant "paints" the UI: every assistant action is a thin wrapper over a store action. searchEmails writes results into the store → the list re-renders. sendEmail opens the compose modal and streams characters into the fields. Because the store is shared, the AI and the human manipulate the exact same state.


Tech stack

  • Next.js 16 (App Router) + TypeScript + Tailwind CSS v4
  • CopilotKit (react-core, react-ui, runtime) — agent↔UI + chat sidebar
  • Ollama via its OpenAI-compatible API (CopilotKit OpenAIAdapter)
  • googleapis (Gmail) + iron-session (encrypted cookie tokens)
  • Zustand (state), Zod (request validation), Vitest (tests)

Setup

Prerequisites

  • Node.js 20+ and npm
  • A Google account (personal Gmail is fine)
  • A running Ollama instance with a tool-capable model pulled (qwen2.5, llama3.1, llama3.3, or mistral-nemo)

1. Google Cloud — Gmail API + OAuth

  1. Create/select a project at https://console.cloud.google.com.
  2. Enable the Gmail API (APIs & Services → Library → Gmail API → Enable).
  3. OAuth consent screen → Audience: External → add your Gmail address under Test users. Keep the app in testing (do not publish; our scopes are "restricted" and publishing would require verification).
  4. Credentials → Create credentials → OAuth client ID → Web application. Add an authorized redirect URI: http://localhost:4321/api/auth/google/callback
  5. Copy the Client ID and Client secret.

Scopes used: gmail.readonly, gmail.send, gmail.modify, plus openid email profile.

2. Environment

cp .env.example .env.local

Fill in .env.local:

GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=http://localhost:4321/api/auth/google/callback
SESSION_SECRET=            # openssl rand -base64 32
OLLAMA_BASE_URL=http://your-ollama-host:11434
OLLAMA_MODEL=qwen2.5

3. Run

npm install
npm run dev -- -p 4321      # http://localhost:4321

Sign in with Google, then try the assistant:

  • "Show me unread emails from the last 7 days"
  • "Open the latest email from "
  • While reading one: "reply to this and say I'll join the call"
  • "Send an email to with subject Hello and body Testing the copilot"

Scripts

npm run dev         # dev server
npm run build       # production build
npm test            # unit tests (Vitest)
npm run typecheck   # tsc --noEmit

Building: stop the dev server first (a concurrent build and dev server contend for the .next dir and can OOM the build worker). If the build worker still runs out of memory, raise Node's heap: NODE_OPTIONS=--max-old-space-size=6144 npm run build.


Design decisions & trade-offs

  • Full-stack Next.js instead of a separate backend. One deployable, one language; Next route handlers give a real backend (OAuth, Gmail, SSE, LLM runtime) without a second service. Clean separation is enforced by module boundaries (lib/mail vs components vs hooks/AI), not by process split.
  • CopilotKit over a hand-rolled agent loop. It provides the exact primitives this needs — frontend actions (useCopilotAction), readable context (useCopilotReadable), generative UI in chat, and human-in-the-loop — and is the modern standard (it authored the AG-UI protocol).
  • Self-hosted Ollama. No hosted-LLM key required. Because small local models are weaker at tool-calling, the action set is deliberately tight, parameters are described precisely, tool calls run sequentially (disableParallelToolCalls), and a firm system prompt steers behavior.
  • Sandboxed iframe for HTML email. Untrusted email markup renders in an iframe with sandbox="" (no scripts, no app access) — safer than injecting HTML, no extra sanitizer dependency.
  • Real-time via polling, not Pub/Sub. For a local demo, an SSE endpoint that polls the Gmail History API needs no public webhook. The production path (Gmail watch → Pub/Sub → webhook) is a drop-in replacement behind the same client contract.
  • Tokens in an encrypted, http-only cookie (iron-session) — never exposed to client JS; all Gmail/LLM calls happen server-side.

Project layout

src/
  app/
    api/{auth,mail,sync,copilotkit}/…   # backend route handlers
    layout.tsx, page.tsx, globals.css
  lib/
    mail/{gmail-service,query,parse,types}.ts   # mail service
    google/{oauth,session,types}.ts             # auth
    ai/{instructions,typewriter}.ts             # assistant helpers
    api-client.ts, compose-helpers.ts, format.ts, cn.ts
  store/appStore.ts                             # single source of truth
  hooks/{useMailCopilot,useMailSync}.tsx
  components/…                                  # UI + assistant components

Licence

MIT. Not affiliated with or endorsed by Google; Gmail is a trademark of Google LLC.

About

An AI mail client where the assistant drives the UI — compose, search, open and reply by natural language, against real Gmail with a self-hosted Ollama model.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages