By Sagar Shankaran, Founder of CallSphere
Learn how to build an AI agent that connects to your inbox via IMAP and the Gmail API, classifies incoming messages by intent, and generates context-aware draft responses automatically.
Key takeaways
Despite the rise of Slack, Teams, and dozens of other messaging platforms, email remains the backbone of external business communication. The average knowledge worker receives over 120 emails per day. Most of those emails fall into predictable categories: meeting requests, customer inquiries, status updates, vendor follow-ups, and internal approvals. An AI agent that can read, classify, and draft responses to these messages saves hours of repetitive work every week.
In this guide, we will build a complete email automation agent using Python. The agent connects to an inbox, classifies each message by intent, selects an appropriate response template, and generates a tailored draft. We will use both IMAP for universal mailbox access and the Gmail API for Google Workspace environments.
IMAP is the universal protocol for reading email. Every major email provider supports it. Python's imaplib handles the connection, and the email module parses message content:
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
import imaplib
import email
from email.header import decode_header
from dataclasses import dataclass
@dataclass
class ParsedEmail:
sender: str
subject: str
body: str
date: str
message_id: str
def connect_and_fetch(
host: str, username: str, password: str, folder: str = "INBOX", limit: int = 20
) -> list[ParsedEmail]:
"""Connect to an IMAP server and fetch recent unread emails."""
conn = imaplib.IMAP4_SSL(host)
conn.login(username, password)
conn.select(folder)
status, message_ids = conn.search(None, "UNSEEN")
ids = message_ids[0].split()[-limit:]
results = []
for mid in ids:
_, msg_data = conn.fetch(mid, "(RFC822)")
raw = email.message_from_bytes(msg_data[0][1])
subject, encoding = decode_header(raw["Subject"])[0]
if isinstance(subject, bytes):
subject = subject.decode(encoding or "utf-8")
body = ""
if raw.is_multipart():
for part in raw.walk():
if part.get_content_type() == "text/plain":
body = part.get_payload(decode=True).decode("utf-8", errors="replace")
break
else:
body = raw.get_payload(decode=True).decode("utf-8", errors="replace")
results.append(ParsedEmail(
sender=raw["From"],
subject=subject,
body=body[:3000],
date=raw["Date"],
message_id=raw["Message-ID"],
))
conn.logout()
return results
The function fetches up to 20 unread messages, parses headers and body text, and returns structured ParsedEmail objects. Truncating the body to 3000 characters keeps LLM token costs reasonable.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Once we have parsed emails, the agent classifies each one. Classification determines which response template to use and whether the email needs human attention:
from openai import OpenAI
CATEGORIES = [
"meeting_request",
"customer_inquiry",
"vendor_followup",
"status_update",
"action_required",
"spam_or_marketing",
"personal",
]
client = OpenAI()
def classify_email(parsed: ParsedEmail) -> dict:
"""Classify an email into a category with confidence."""
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": (
"You classify emails. Return JSON with keys: "
"category (one of: " + ", ".join(CATEGORIES) + "), "
"confidence (float 0-1), "
"summary (one sentence), "
"urgency (low, medium, high)."
),
},
{
"role": "user",
"content": f"From: {parsed.sender}\nSubject: {parsed.subject}\n\n{parsed.body}",
},
],
)
import json
return json.loads(response.choices[0].message.content)
Using response_format={"type": "json_object"} ensures the model returns valid JSON every time. The classification includes a confidence score so the agent can flag uncertain cases for human review.
With classification complete, the agent generates a draft response. Each category maps to a system prompt that constrains the tone and content:
RESPONSE_PROMPTS = {
"meeting_request": (
"Draft a polite reply to this meeting request. "
"Confirm availability or suggest alternative times. Keep it under 100 words."
),
"customer_inquiry": (
"Draft a helpful reply to this customer question. "
"Be professional and thorough. Ask clarifying questions if needed."
),
"vendor_followup": (
"Draft a brief professional reply acknowledging this vendor message."
),
"action_required": (
"Draft a reply acknowledging receipt and confirming you will address the request."
),
}
def generate_draft(parsed: ParsedEmail, classification: dict) -> str | None:
"""Generate a draft response based on email classification."""
category = classification["category"]
if category in ("spam_or_marketing", "status_update", "personal"):
return None # No auto-reply for these categories
system_prompt = RESPONSE_PROMPTS.get(category, RESPONSE_PROMPTS["action_required"])
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0.4,
messages=[
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": (
f"Original email from {parsed.sender}:\n"
f"Subject: {parsed.subject}\n\n{parsed.body}"
),
},
],
)
return response.choices[0].message.content
The agent skips spam, status updates, and personal messages entirely. For everything else, it generates a contextual draft that a human can review before sending.
For Google Workspace users, the Gmail API lets you save drafts directly into the inbox:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import base64
from email.mime.text import MIMEText
from googleapiclient.discovery import build
def save_gmail_draft(service, to: str, subject: str, body: str) -> str:
"""Save a draft reply in Gmail."""
message = MIMEText(body)
message["to"] = to
message["subject"] = f"Re: {subject}"
raw = base64.urlsafe_b64encode(message.as_bytes()).decode()
draft = service.users().drafts().create(
userId="me",
body={"message": {"raw": raw}},
).execute()
return draft["id"]
The main loop ties everything together, processing each unread email through the classify-then-respond pipeline:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("email_agent")
def run_email_agent(imap_host: str, username: str, password: str):
"""Main loop: fetch, classify, draft responses."""
emails = connect_and_fetch(imap_host, username, password)
logger.info(f"Fetched {len(emails)} unread emails")
for parsed in emails:
classification = classify_email(parsed)
logger.info(
f"[{classification['category']}] {parsed.subject} "
f"(confidence: {classification['confidence']}, urgency: {classification['urgency']})"
)
draft = generate_draft(parsed, classification)
if draft:
logger.info(f" Draft generated ({len(draft)} chars)")
# save_gmail_draft(service, parsed.sender, parsed.subject, draft)
else:
logger.info(" Skipped (no reply needed)")
Extract attachments using part.get_filename() inside the multipart walk loop. Save them to disk or cloud storage, then include a summary of attachment names and types in the classification prompt so the LLM can factor them into its response.
Start with draft-only mode where the agent creates drafts for human approval. Once you have validated accuracy over a few hundred emails and the confidence threshold is above 0.9, you can enable auto-send for low-risk categories like meeting confirmations and acknowledgments.
Add a sender filter that checks for common no-reply patterns like noreply@, no-reply@, and mailer-daemon@. Skip classification entirely for these senders to avoid wasting API calls.
#EmailAutomation #AIAgents #GmailAPI #IMAP #WorkflowAutomation #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.