Build an AI Email Summarizer in Python

# Build an AI Email Summarizer in Python

Email overload is real. If you’re like most developers, you spend 30+ minutes daily just reading through inboxes to find the one message that actually needs a response. AI summarization can cut that time down to seconds—and you can build it yourself without relying on expensive third-party tools.

This article shows you exactly how to implement email summarization using Python and modern LLMs. We’ll cover the architecture, code, API options, and the gotchas you’ll hit when running this in production.

## The Problem: Your Inbox Is a Time Sink

Most email APIs give you raw access to messages—subject, body, sender, timestamp. What they don’t give you is the *meaning*. You’re still stuck parsing hundreds of messages to find the signal.

AI summarization solves this by extracting the key information from each email:
– What is this email about?
– Who does it require action from?
– What’s the deadline or urgency level?

Instead of reading every email, you read the summary. Simple, but the implementation details matter.

## How AI Email Summarization Works

The architecture is straightforward. You have three main components:

1. **Email Fetching** – Pull messages from Gmail, Outlook, or IMAP
2. **Prompt Engineering** – Craft instructions for the LLM about what to extract
3. **LLM Processing** – Send the email content through an API and get back a summary

The critical piece is step 2. The prompt determines whether you get useful output or generic garbage. A good email summarization prompt needs to specify:
– The output format (JSON, bullet points, etc.)
– What fields to extract (sender, action items, urgency, deadline)
– Constraints (length limits, language style)

Here’s the basic flow:

“`
Email API → Raw Message → Prompt + Message → LLM API → Structured Summary → Your App
“`

## A Python Implementation

Let’s build a working summarizer. This example uses OpenAI’s API, but I’ll show alternatives later.

“`python
import os
import json
from openai import OpenAI
from typing import Optional
from dataclasses import dataclass

@dataclass
class EmailSummary:
category: str # meeting, invoice, bug report, etc.
action_required: str # “reply”, “review PR”, “schedule call”, “none”
urgency: str # “high”, “medium”, “low”
key_points: list[str]
sender: str

class EmailSummarizer:
def __init__(self, api_key: Optional[str] = None):
self.client = OpenAI(api_key=api_key or os.getenv(“OPENAI_API_KEY”))

def summarize(self, subject: str, body: str, sender: str) -> EmailSummary:
prompt = f”””You are an email analysis assistant. Analyze this email and return a JSON object with these exact fields:
– category: One of [meeting, invoice, bug_report, code_review, general, urgent, recruitment]
– action_required: What the recipient needs to do, or “none” if no action needed
– urgency: “high”, “medium”, or “low”
– key_points: 2-4 bullet points summarizing the email content
– sender: The sender’s name or email

Email:
Subject: {subject}
From: {sender}
Body: {body}

Return ONLY valid JSON, no explanation.”””

response = self.client.chat.completions.create(
model=”gpt-4o-mini”,
messages=[{“role”: “user”, “content”: prompt}],
temperature=0.3, # Low temperature for consistent output
response_format={“type”: “json_object”}
)

result = json.loads(response.choices[0].message.content)
return EmailSummary(
category=result.get(“category”, “general”),
action_required=result.get(“action_required”, “none”),
urgency=result.get(“urgency”, “low”),
key_points=result.get(“key_points”, []),
sender=result.get(“sender”, sender)
)
“`

This gives you structured output you can filter, sort, or display however you want. Want to see only high-urgency emails? Easy. Want to group by category? Also easy.

## Connecting to Your Email Provider

The summarizer above works with any email. Here’s how to wire it up to Gmail:

“`python
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from google.auth.transport.requests import Request

def fetch_recent_emails(max_results: int = 10):
creds = Credentials.from_authorized_user_info(
json.loads(os.getenv(“GMAIL_CREDENTIALS_JSON”))
)

service = build(‘gmail’, ‘v1′, credentials=creds)

results = service.users().messages().list(
userId=’me’,
maxResults=max_results,
labelIds=[‘INBOX’]
).execute()

messages = []
for msg in results.get(‘messages’, []):
full_msg = service.users().messages().get(
userId=’me’, id=msg[‘id’], format=’full’
).execute()

headers = {h[‘name’]: h[‘value’] for h in full_msg[‘payload’][‘headers’]}
body = full_msg[‘payload’][‘body’].get(‘data’, ”)

messages.append({
‘subject’: headers.get(‘Subject’, ”),
‘sender’: headers.get(‘From’, ”),
‘body’: body
})

return messages

# Usage
summarizer = EmailSummarizer()
emails = fetch_recent_emails()

for email in emails:
summary = summarizer.summarize(
email[‘subject’],
email[‘body’],
email[‘sender’]
)
print(f”[{summary.urgency.upper()}] {summary.category}: {summary.action_required}”)
“`

This fetches your inbox and prints a filtered view. You could just as easily send these summaries to Slack, a dashboard, or a mobile notification.

## API Options: Trade-offs to Consider

You’re not locked into OpenAI. Here’s how the main options stack up:

| Provider | Model | Strengths | Weaknesses |
|———-|——-|———–|————|
| OpenAI | gpt-4o-mini | Fast, cheap, reliable | API leaves your data |
| Anthropic | claude-3-haiku | Good reasoning, competitive pricing | Slightly more expensive |
| Google | gemini-1.5-flash | Free tier available | Less fine-tuned for extraction |
| Ollama | local models | Full privacy, no API costs | Requires GPU hardware |

For most use cases, **gpt-4o-mini** is the sweet spot—it’s fast ($0.15/1M input tokens), accurate enough for email extraction, and the API is reliable.

If you’re handling sensitive data (client emails, internal communications), consider running a local model via Ollama. The trade-off is latency and hardware cost—you’ll need a decent GPU to run it acceptably.

## Edge Cases That Will Break Your Pipeline

Three things will cause you problems in production:

1. **HTML email bodies** – Most emails are HTML, not plain text. The raw body data will be base64 encoded or contain HTML tags. You need to parse and strip HTML before sending to the LLM.

2. **Very long emails** – LLMs have context limits (typically 128k tokens for modern models). Threaded email conversations can hit this fast. Truncate or split long emails.

3. **Non-English emails** – The prompt I showed works best in English. If you’re handling multilingual inboxes, either use a multilingual model or detect the language first and route to language-specific prompts.

Here’s a fix for the HTML problem:

“`python
import base64
from bs4 import BeautifulSoup

def decode_email_body(payload: dict) -> str:
“””Extract plain text from Gmail email payload.”””
if payload.get(‘body’, {}).get(‘data’):
data = payload[‘body’][‘data’]
decoded = base64.urlsafe_b64decode(data).decode(‘utf-8’)

# Check if it’s HTML
if ‘