🤖AI Agents Guide
TutorialsComparisonsReviewsExamplesIntegrationsUse CasesTemplatesGlossary
Get Started
🤖AI Agents Guide

Your comprehensive resource for understanding, building, and implementing AI Agents.

Learn

  • Tutorials
  • Glossary
  • Use Cases
  • Examples

Compare

  • Tool Comparisons
  • Reviews
  • Integrations
  • Templates

Company

  • About
  • Contact
  • Privacy Policy

© 2026 AI Agents Guide. All rights reserved.

Home/Tutorials/Build an Email Management AI Agent
intermediate34 min read

Build an Email Management AI Agent

Learn how to build an AI email agent that reads, classifies, drafts, and sends emails using the Gmail API and LangChain, with intelligent triage logic, priority scoring, and auto-draft workflows for high-volume inboxes.

Laptop on pedestal with abstract sculptures and spherical art
Photo by Route4design on Unsplash
By AI Agents Guide Team•February 28, 2026

Table of Contents

  1. What You'll Learn
  2. Prerequisites
  3. Architecture Overview
  4. Step 1: Gmail API Setup
  5. Step 2: Gmail Authentication
  6. Step 3: Email Reading and Parsing Tools
  7. Step 4: Email Classification
  8. Step 5: Reply Drafting and Send Tool
  9. Step 6: The Email Agent Loop
  10. What's Next
A snake looks up into the brightness.
Photo by Magdalena Grabowska on Unsplash

Build an Email Management AI Agent in Python

The average knowledge worker spends 28% of their workday managing email. Most of that time is spent on mechanical tasks: reading threads to understand context, deciding what needs a reply, drafting similar responses, and organizing messages into folders. An AI email agent can handle all of these tasks — reading emails, classifying them by urgency and topic, drafting replies in your voice, and sending or forwarding with your approval.

This tutorial builds a practical email management agent that connects to Gmail via the Gmail API, classifies incoming emails, drafts contextually appropriate replies, and integrates a human-approval gate before any email is actually sent. It is designed to be a starting point you can adapt to your specific workflow.

What You'll Learn#

  • How to authenticate with the Gmail API using OAuth2
  • How to build LangChain tools for reading, searching, and drafting emails
  • How to classify emails by priority and topic using LLM-structured output
  • How to maintain thread context for coherent reply drafting
  • How to integrate approval gates before sending to prevent accidental sends

Prerequisites#

  • Python 3.10+
  • Google account with Gmail API enabled (see Step 1)
  • OpenAI API key
  • Basic understanding of LangChain agent patterns
  • Familiarity with AI agent concepts

Architecture Overview#

The agent has three operational modes:

  1. Triage Mode — Reads the last N unread emails, classifies each by priority and topic, and returns a structured triage report
  2. Reply Mode — Given an email thread, drafts a contextually appropriate reply in the user's style
  3. Compose Mode — Drafts a new outbound email from a natural language description

All modes that involve sending require explicit approval (via the terminal in development, or via the HITL API pattern in production).

Step 1: Gmail API Setup#

  1. Go to console.cloud.google.com ↗
  2. Create a new project (or use existing)
  3. Enable the Gmail API in APIs & Services
  4. Create an OAuth 2.0 Client ID (Desktop Application type)
  5. Download the credentials.json file to your project directory

Install dependencies:

pip install langchain==0.3.0 langchain-openai==0.2.0 \
    google-auth==2.35.0 google-auth-oauthlib==1.2.1 \
    google-auth-httplib2==0.2.0 google-api-python-client==2.147.0 \
    python-dotenv==1.0.1

Step 2: Gmail Authentication#

# gmail_auth.py
import os
import json
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.compose",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.modify",
]
TOKEN_FILE = "token.json"
CREDENTIALS_FILE = "credentials.json"

def get_gmail_service():
    """Authenticate and return an authorized Gmail API service object."""
    creds = None

    if os.path.exists(TOKEN_FILE):
        creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
            creds = flow.run_local_server(port=0)
        with open(TOKEN_FILE, "w") as token:
            token.write(creds.to_json())

    return build("gmail", "v1", credentials=creds)

# Test authentication
if __name__ == "__main__":
    service = get_gmail_service()
    profile = service.users().getProfile(userId="me").execute()
    print(f"Authenticated as: {profile['emailAddress']}")

Run python gmail_auth.py once to complete the OAuth flow and save token.json.

Step 3: Email Reading and Parsing Tools#

# email_tools.py
import base64
import email as email_lib
from typing import Optional
from gmail_auth import get_gmail_service

service = get_gmail_service()

def decode_body(payload: dict) -> str:
    """Extract plain text body from a Gmail message payload."""
    body = ""
    if "parts" in payload:
        for part in payload["parts"]:
            if part["mimeType"] == "text/plain":
                data = part["body"].get("data", "")
                body = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
                break
    elif "body" in payload:
        data = payload["body"].get("data", "")
        if data:
            body = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
    return body

def get_email_headers(headers: list) -> dict:
    """Extract common headers from a Gmail message."""
    header_map = {h["name"].lower(): h["value"] for h in headers}
    return {
        "subject": header_map.get("subject", "(no subject)"),
        "from": header_map.get("from", ""),
        "to": header_map.get("to", ""),
        "date": header_map.get("date", ""),
        "reply_to": header_map.get("reply-to", header_map.get("from", "")),
    }

def list_unread_emails(max_results: int = 10) -> list[dict]:
    """Fetch the most recent unread emails."""
    results = service.users().messages().list(
        userId="me",
        labelIds=["INBOX", "UNREAD"],
        maxResults=max_results,
    ).execute()
    messages = results.get("messages", [])

    emails = []
    for msg in messages:
        full = service.users().messages().get(
            userId="me", id=msg["id"], format="full"
        ).execute()
        payload = full.get("payload", {})
        headers = get_email_headers(payload.get("headers", []))
        body = decode_body(payload)

        emails.append({
            "id": msg["id"],
            "thread_id": full.get("threadId"),
            "subject": headers["subject"],
            "from": headers["from"],
            "to": headers["to"],
            "date": headers["date"],
            "body": body[:2000],  # Truncate to avoid token overflow
            "snippet": full.get("snippet", ""),
        })
    return emails

def get_thread(thread_id: str) -> list[dict]:
    """Fetch all messages in a thread."""
    thread = service.users().threads().get(userId="me", id=thread_id).execute()
    messages = []
    for msg in thread.get("messages", []):
        payload = msg.get("payload", {})
        headers = get_email_headers(payload.get("headers", []))
        body = decode_body(payload)
        messages.append({
            "id": msg["id"],
            "from": headers["from"],
            "date": headers["date"],
            "body": body[:1500],
        })
    return messages

Step 4: Email Classification#

# classifier.py
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

class EmailClassification(BaseModel):
    priority: str = Field(description="urgent, high, medium, or low")
    category: str = Field(description="customer, internal, newsletter, spam, support, sales, personal")
    requires_reply: bool = Field(description="True if this email requires a human response")
    sentiment: str = Field(description="positive, neutral, negative")
    summary: str = Field(description="One sentence summary of the email")
    suggested_action: str = Field(description="Brief action recommendation")

classifier_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
classifier_chain = ChatPromptTemplate.from_messages([
    ("system", """You are an email triage assistant.
Classify emails by priority, category, and required action.
Priority guidelines: urgent=needs response within 1h, high=today, medium=this week, low=whenever."""),
    ("human", "From: {sender}\nSubject: {subject}\n\nBody:\n{body}"),
]) | classifier_llm.with_structured_output(EmailClassification)

def classify_email(email: dict) -> EmailClassification:
    return classifier_chain.invoke({
        "sender": email["from"],
        "subject": email["subject"],
        "body": email["body"],
    })

Email agent triage report showing classified emails with priority scores and suggested actions

Step 5: Reply Drafting and Send Tool#

# draft_and_send.py
import base64
from email.mime.text import MIMEText
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from gmail_auth import get_gmail_service
from email_tools import get_thread

service = get_gmail_service()

def draft_reply(thread_id: str, user_instructions: str = "") -> str:
    """Draft a reply to an email thread using the full thread context."""
    thread_messages = get_thread(thread_id)
    thread_text = "\n\n---\n".join(
        f"From: {m['from']}\n{m['body']}" for m in thread_messages
    )
    llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
    chain = ChatPromptTemplate.from_messages([
        ("system", """You are drafting an email reply.
Write in a professional, concise style. Match the tone of the thread.
Do not add unnecessary pleasantries. Get to the point quickly.
{instructions}"""),
        ("human", "Thread:\n{thread}\n\nWrite a reply:"),
    ]) | llm

    result = chain.invoke({
        "thread": thread_text,
        "instructions": f"Additional instructions: {user_instructions}" if user_instructions else "",
    })
    return result.content

def send_email(to: str, subject: str, body: str, thread_id: str = None) -> str:
    """Create and send an email. Returns the sent message ID."""
    msg = MIMEText(body)
    msg["to"] = to
    msg["subject"] = subject

    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
    send_body = {"raw": raw}
    if thread_id:
        send_body["threadId"] = thread_id

    result = service.users().messages().send(
        userId="me", body=send_body
    ).execute()
    return f"Sent. Message ID: {result['id']}"

def create_draft(to: str, subject: str, body: str) -> str:
    """Save as draft without sending — safer for human review."""
    msg = MIMEText(body)
    msg["to"] = to
    msg["subject"] = subject
    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
    result = service.users().drafts().create(
        userId="me", body={"message": {"raw": raw}}
    ).execute()
    return f"Draft saved. Draft ID: {result['id']}"

Step 6: The Email Agent Loop#

# agent.py
from langchain_openai import ChatOpenAI
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from email_tools import list_unread_emails
from classifier import classify_email
from draft_and_send import draft_reply, send_email, create_draft

@tool
def triage_inbox(max_emails: int = 10) -> str:
    """Read and classify the most recent unread emails. Returns a triage report."""
    emails = list_unread_emails(max_emails)
    if not emails:
        return "No unread emails found."

    lines = ["Email Triage Report", "=" * 40]
    for email in emails:
        clf = classify_email(email)
        lines.append(
            f"[{clf.priority.upper()}] {email['subject']}\n"
            f"  From: {email['from']}\n"
            f"  Category: {clf.category} | Reply needed: {clf.requires_reply}\n"
            f"  Summary: {clf.summary}\n"
            f"  Action: {clf.suggested_action}\n"
        )
    return "\n".join(lines)

@tool
def draft_email_reply(thread_id: str, instructions: str = "") -> str:
    """Draft a reply to an email thread. Saves as a draft — does not send."""
    draft_text = draft_reply(thread_id, instructions)
    return f"Draft created:\n\n{draft_text}"

@tool
def save_draft_to_gmail(to: str, subject: str, body: str) -> str:
    """Save a composed email as a Gmail draft for human review before sending."""
    return create_draft(to, subject, body)

def build_email_agent() -> AgentExecutor:
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    tools = [triage_inbox, draft_email_reply, save_draft_to_gmail]
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are an email management assistant.
Help triage emails, draft replies, and organize the inbox.
IMPORTANT: Always save emails as drafts — never call send_email directly.
The human will review and approve drafts before they are sent."""),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ])
    agent = create_openai_tools_agent(llm, tools, prompt)
    return AgentExecutor(agent=agent, tools=tools, verbose=True)

Note that send_email is intentionally excluded from the agent's tool list. The agent can only create drafts, enforcing the human-approval pattern described in the HITL tutorial. Add Langfuse observability to audit which emails the agent processes and what actions it takes.

What's Next#

  • Add approval workflows before sending using human-in-the-loop patterns
  • Deploy this agent as a scheduled service with the Docker deployment guide
  • Connect email insights to a data analysis pipeline with the data analyst agent
  • Review AI agent security best practices for handling sensitive email content
  • Build a companion LangGraph multi-agent system that routes different email types to specialized sub-agents

Related Tutorials

How to Create a Meeting Scheduling AI Agent

Build an autonomous AI agent to handle meeting scheduling, calendar checks, and bookings intelligently. This step-by-step tutorial covers Python implementation with LangChain, Google Calendar integration, and advanced features like conflict resolution for efficient automation.

How to Manage Multiple AI Agents

Master managing multiple AI agents with this in-depth tutorial. Learn orchestration, state sharing, parallel execution, and scaling using LangGraph and custom tools. From basics to production-ready swarms for complex tasks.

How to Train an AI Agent on Your Own Data

Master training AI agents on custom data with three methods: context stuffing, RAG using vector databases, and fine-tuning. This beginner-to-advanced guide includes step-by-step code examples, pitfalls, and best practices to build knowledgeable agents for your specific needs.

← Back to All Tutorials