Automate Email with AI: Build a Smart Email Processor Using Python and Claude
Build an AI email automation system that classifies, prioritizes, and drafts responses to incoming emails. Full Python code using Gmail API and Claude — from zero inbox to zero effort.
The average professional receives 121 emails per day. Of those, maybe 10 require thoughtful responses, 30 need a quick acknowledgment, and the rest are newsletters, notifications, and CC’d threads you never asked to be part of.
What if an AI could handle the triage? Classify every incoming email, draft responses for the straightforward ones, flag the important ones, and quietly archive the noise — all before you open your inbox.
That’s what we’re building. A Python script that connects to your Gmail, uses Claude to understand each email, and takes action based on intelligent classification. No third-party apps. No monthly subscriptions. Just your Gmail API credentials and an Anthropic API key.
What We’re Building
Incoming email
→ Fetch from Gmail API
→ Send to Claude for classification
→ Claude returns:
├── Category: urgent / needs-reply / informational / spam
├── Priority: high / medium / low
├── Summary: one-sentence description
└── Draft reply (if needs-reply)
→ Take action:
├── Urgent: Label "URGENT", send Slack notification
├── Needs reply: Save draft response in Gmail
├── Informational: Label and archive
└── Spam/noise: Archive immediately
Prerequisites
- Python 3.11+
- Google Cloud project with Gmail API enabled
- Anthropic API key
- Gmail account
Step 1: Set Up Gmail API
Create Google Cloud Project
- Go to Google Cloud Console
- Create a new project (or select existing)
- Enable the Gmail API:
- Navigate to APIs & Services > Library
- Search “Gmail API”
- Click Enable
Create OAuth Credentials
- Go to APIs & Services > Credentials
- Click “Create Credentials” > “OAuth 2.0 Client ID”
- Application type: “Desktop application”
- Download the JSON file as
credentials.json
Install Dependencies
mkdir email-ai && cd email-ai
python3 -m venv venv
source venv/bin/activate
pip install google-auth-oauthlib google-api-python-client anthropic python-dotenv
Step 2: Gmail Authentication
# gmail_auth.py
"""Handle Gmail API authentication."""
import os
import pickle
from google.auth.transport.requests import Request
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.modify',
'https://www.googleapis.com/auth/gmail.compose',
]
def get_gmail_service():
"""Authenticate and return Gmail API service."""
creds = None
# Load saved credentials
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# Refresh or create new credentials
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.json', SCOPES
)
creds = flow.run_local_server(port=0)
# Save credentials for future runs
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
return build('gmail', 'v1', credentials=creds)
Step 3: Email Fetcher
# email_fetcher.py
"""Fetch and parse emails from Gmail."""
import base64
from email.mime.text import MIMEText
from typing import Optional
def get_unread_emails(service, max_results: int = 20) -> list[dict]:
"""Fetch unread emails from inbox."""
results = service.users().messages().list(
userId='me',
labelIds=['INBOX', 'UNREAD'],
maxResults=max_results
).execute()
messages = results.get('messages', [])
emails = []
for msg_ref in messages:
msg = service.users().messages().get(
userId='me',
id=msg_ref['id'],
format='full'
).execute()
email_data = parse_email(msg)
if email_data:
emails.append(email_data)
return emails
def parse_email(message: dict) -> Optional[dict]:
"""Parse a Gmail message into a clean dict."""
headers = message.get('payload', {}).get('headers', [])
def get_header(name: str) -> str:
for h in headers:
if h['name'].lower() == name.lower():
return h['value']
return ''
# Extract body
body = extract_body(message.get('payload', {}))
return {
'id': message['id'],
'thread_id': message['threadId'],
'from': get_header('From'),
'to': get_header('To'),
'subject': get_header('Subject'),
'date': get_header('Date'),
'body': body[:3000], # Limit body length for API
'snippet': message.get('snippet', ''),
'labels': message.get('labelIds', []),
}
def extract_body(payload: dict) -> str:
"""Extract plain text body from email payload."""
if payload.get('mimeType') == 'text/plain':
data = payload.get('body', {}).get('data', '')
if data:
return base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
# Check parts for multipart messages
for part in payload.get('parts', []):
if part.get('mimeType') == 'text/plain':
data = part.get('body', {}).get('data', '')
if data:
return base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
# Recurse into nested parts
if part.get('parts'):
result = extract_body(part)
if result:
return result
return ''
def create_draft(service, to: str, subject: str, body: str,
thread_id: Optional[str] = None) -> dict:
"""Create a draft reply in Gmail."""
message = MIMEText(body)
message['to'] = to
message['subject'] = subject
raw = base64.urlsafe_b64encode(
message.as_bytes()
).decode('utf-8')
draft_body = {'message': {'raw': raw}}
if thread_id:
draft_body['message']['threadId'] = thread_id
return service.users().drafts().create(
userId='me',
body=draft_body
).execute()
def add_label(service, message_id: str, label_ids: list[str]) -> None:
"""Add labels to a message."""
service.users().messages().modify(
userId='me',
id=message_id,
body={'addLabelIds': label_ids}
).execute()
def archive_message(service, message_id: str) -> None:
"""Remove INBOX label (archive)."""
service.users().messages().modify(
userId='me',
id=message_id,
body={'removeLabelIds': ['INBOX']}
).execute()
Step 4: AI Email Classifier
# ai_classifier.py
"""Use Claude to classify and respond to emails."""
import json
import anthropic
from typing import Optional
class EmailClassifier:
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def classify_email(self, email: dict) -> dict:
"""Classify an email and optionally draft a response."""
prompt = f"""Analyze this email and return a JSON response.
From: {email['from']}
Subject: {email['subject']}
Date: {email['date']}
Body:
{email['body']}
Return ONLY valid JSON with these fields:
{{
"category": "urgent|needs-reply|informational|noise",
"priority": "high|medium|low",
"summary": "one sentence summary",
"reasoning": "brief explanation of classification",
"needs_response": true/false,
"suggested_reply": "draft reply text if needs_response is true, null otherwise",
"action": "draft-reply|label-archive|archive|flag"
}}
Classification rules:
- urgent: Time-sensitive requests, payment issues, system alerts, boss/client requests
- needs-reply: Questions directed at the recipient, meeting requests, action items
- informational: Newsletters, updates, FYI emails, automated reports
- noise: Marketing, promotions, social notifications, CC'd threads with no action needed
For suggested_reply:
- Keep it professional and concise
- Match the tone of the original email
- If unclear what to respond, set needs_response to false"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=(
"You are an email triage assistant. Classify emails accurately "
"and draft helpful responses. Always return valid JSON."
),
messages=[{"role": "user", "content": prompt}]
)
result_text = response.content[0].text
# Parse JSON from response (handle markdown code blocks)
if "```json" in result_text:
result_text = result_text.split("```json")[1].split("```")[0]
elif "```" in result_text:
result_text = result_text.split("```")[1].split("```")[0]
try:
return json.loads(result_text.strip())
except json.JSONDecodeError:
return {
"category": "informational",
"priority": "low",
"summary": "Could not classify",
"reasoning": "JSON parsing failed",
"needs_response": False,
"suggested_reply": None,
"action": "label-archive"
}
Step 5: Main Processor
# processor.py
"""Main email processing pipeline."""
import os
import json
from datetime import datetime
from dotenv import load_dotenv
from gmail_auth import get_gmail_service
from email_fetcher import (
get_unread_emails, create_draft, add_label, archive_message
)
from ai_classifier import EmailClassifier
load_dotenv()
def ensure_labels(service) -> dict:
"""Create custom labels if they don't exist."""
existing = service.users().labels().list(userId='me').execute()
existing_names = {l['name']: l['id'] for l in existing.get('labels', [])}
needed = ['AI/Urgent', 'AI/NeedsReply', 'AI/Informational', 'AI/Processed']
label_ids = {}
for name in needed:
if name in existing_names:
label_ids[name] = existing_names[name]
else:
label = service.users().labels().create(
userId='me',
body={
'name': name,
'labelListVisibility': 'labelShow',
'messageListVisibility': 'show',
}
).execute()
label_ids[name] = label['id']
return label_ids
def process_emails():
"""Main processing function."""
print(f"\n{'='*60}")
print(f"Email AI Processor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'='*60}\n")
# Initialize services
service = get_gmail_service()
classifier = EmailClassifier(api_key=os.getenv('ANTHROPIC_API_KEY'))
labels = ensure_labels(service)
# Fetch unread emails
emails = get_unread_emails(service, max_results=10)
print(f"Found {len(emails)} unread emails\n")
results = []
for email in emails:
print(f"Processing: {email['subject'][:60]}...")
# Classify with AI
classification = classifier.classify_email(email)
# Take action based on classification
action = classification.get('action', 'label-archive')
category = classification.get('category', 'informational')
if category == 'urgent':
add_label(service, email['id'], [labels['AI/Urgent']])
print(f" → URGENT: {classification['summary']}")
elif category == 'needs-reply' and classification.get('suggested_reply'):
# Create draft reply
sender = email['from']
subject = email['subject']
if not subject.startswith('Re:'):
subject = f"Re: {subject}"
create_draft(
service,
to=sender,
subject=subject,
body=classification['suggested_reply'],
thread_id=email['thread_id']
)
add_label(service, email['id'], [labels['AI/NeedsReply']])
print(f" → DRAFT CREATED: {classification['summary']}")
elif category == 'informational':
add_label(service, email['id'], [labels['AI/Informational']])
archive_message(service, email['id'])
print(f" → ARCHIVED: {classification['summary']}")
elif category == 'noise':
archive_message(service, email['id'])
print(f" → NOISE (archived): {classification['summary']}")
# Mark as processed
add_label(service, email['id'], [labels['AI/Processed']])
results.append({
'subject': email['subject'],
'from': email['from'],
**classification
})
# Print summary
print(f"\n{'='*60}")
print("SUMMARY")
print(f"{'='*60}")
for r in results:
icon = {'urgent': '🔴', 'needs-reply': '🟡',
'informational': '🔵', 'noise': '⚪'}.get(r['category'], '⚪')
print(f"{icon} [{r['priority'].upper()}] {r['summary']}")
# Save log
log_file = f"logs/email_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
os.makedirs('logs', exist_ok=True)
with open(log_file, 'w') as f:
json.dump(results, f, indent=2)
print(f"\nLog saved to {log_file}")
if __name__ == '__main__':
process_emails()
Step 6: Schedule Automated Runs
Using cron (Linux/macOS)
# Run every 15 minutes
crontab -e
# Add this line:
*/15 * * * * cd /path/to/email-ai && /path/to/email-ai/venv/bin/python processor.py >> /path/to/email-ai/logs/cron.log 2>&1
Using systemd timer (Linux)
# Create service file
sudo tee /etc/systemd/system/email-ai.service << 'EOF'
[Unit]
Description=AI Email Processor
[Service]
Type=oneshot
User=your-username
WorkingDirectory=/path/to/email-ai
ExecStart=/path/to/email-ai/venv/bin/python processor.py
EOF
# Create timer file
sudo tee /etc/systemd/system/email-ai.timer << 'EOF'
[Unit]
Description=Run AI Email Processor every 15 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
EOF
sudo systemctl enable email-ai.timer
sudo systemctl start email-ai.timer
Cost Analysis
Per email processed:
- Input: ~800 tokens (email content + prompt)
- Output: ~200 tokens (classification + draft)
- Cost per email (Claude Sonnet): ~$0.005
Daily (50 emails processed):
- $0.25/day
- $7.50/month
Compare to commercial alternatives:
- SaneBox: $7/month (no AI drafting)
- Superhuman: $30/month
- Your solution: $7.50/month + full control
Safety Considerations
Never auto-send replies. Always create drafts that you review before sending. AI can misinterpret tone, miss context from previous threads, or draft responses that are technically correct but socially wrong.
Exclude sensitive senders. Add a blocklist for emails from your boss, legal department, HR, or clients where AI processing is inappropriate:
SKIP_SENDERS = [
'ceo@company.com',
'legal@company.com',
'hr@company.com',
]
# In processor.py, before classification:
sender_email = email['from'].lower()
if any(skip in sender_email for skip in SKIP_SENDERS):
print(f" → SKIPPED (sensitive sender)")
continue
Log everything. Keep records of what the AI classified and what actions it took. If something goes wrong, you need an audit trail.
This system handles the email triage that would otherwise eat 30-60 minutes of your morning. It doesn’t replace your judgment — it does the sorting so you can focus your judgment on the emails that actually need your brain.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
> Related Articles
Web Scraping with AI: Build a Smart Data Extraction Pipeline
Traditional web scraping breaks when websites change layouts. AI-powered scraping understands page structure and extracts data intelligently. Here's how to build one using Python, Beautiful Soup, and Claude.
Create an AI Art Portfolio: From Generation to Gallery in One Weekend
Build a professional AI art portfolio website with curated collections, consistent style, and proper attribution. Covers prompt engineering, style consistency, curation, and deployment.
Build an AI Chrome Extension: Add Claude to Any Webpage in 60 Minutes
Build a Chrome extension that summarizes web pages, answers questions about content, and rewrites selected text — all powered by Claude. Full source code and step-by-step instructions included.
Tags
> Stay in the loop
Weekly AI tools & insights.