Tutorial ยท Claude API Integration

How to Build a Claude-Powered Email Assistant for Enterprise Teams

The average executive spends 2.5 hours per day on email. Most of that time is not decision-making โ€” it's triage: reading, classifying, summarising, drafting routine replies, and routing messages to the right people. A well-built Claude email assistant handles all of that automatically, leaving your team to focus on the 20% of messages that actually require human judgment.

This tutorial walks through building a Claude-powered email assistant for enterprise teams โ€” covering triage, smart reply drafting, thread summarisation, and CRM synchronisation. The architecture uses the Claude API with Microsoft Graph API or Gmail API, deployable as a standalone service or as an MCP server for teams using Claude Cowork. If you want a fully managed deployment rather than building it yourself, our Claude API integration service handles the full build.

What This Tutorial Covers

  • Architecture for connecting Claude to Microsoft 365 or Gmail
  • Automated email triage and priority scoring
  • Contextual reply drafting using Claude's extended context window
  • Thread summarisation for executives and long CC chains
  • CRM sync: automatically logging key emails to Salesforce or HubSpot
  • Enterprise security considerations for email AI

System Architecture

A Claude email assistant needs three layers: an email data layer (Microsoft Graph or Gmail API), an intelligence layer (Claude API), and an action layer (write back to email, update CRM, trigger workflows). How you wire these together determines whether you build something your team uses or something that breaks in production after two weeks.

The Data Layer

For Microsoft 365 environments, use the Microsoft Graph API with OAuth 2.0. For Gmail, use the Gmail API via Google's Python client. Both provide webhook/push notification capabilities โ€” critical for real-time processing rather than polling. Register an Azure AD application (for Microsoft) or a Google Cloud project (for Gmail) with the minimum required scopes: Mail.Read, Mail.ReadWrite, Mail.Send. Never request Mail.ReadWrite.All unless your architecture genuinely requires organisation-wide inbox access.

The Intelligence Layer

Claude Sonnet 4 is the right model for most email tasks โ€” it's fast, accurate, and cost-effective at scale. Use Claude Opus 4 only for complex drafting tasks where tone and nuance matter more than cost. Structure your prompts to include: the email content, the recipient's name and role, any relevant thread history, and the specific task (triage/draft/summarise). Use prompt caching for your system prompt and any static company context to reduce costs by 60โ€“80% on high volumes.

The Action Layer

Keep actions reversible. Claude should write draft replies, not send them. Apply labels and priority flags automatically, but let humans move items to folders. Log to CRM automatically, but surface a review queue so sales reps can correct misclassifications. The one exception: auto-archiving newsletters, marketing emails, and automated notifications where the cost of a false positive is low and the volume justifies it.

Setting Up the Integration

Microsoft 365 Authentication

pip install msal msgraph-core anthropic python-dotenv
# auth.py โ€” Microsoft Graph authentication
from msal import ConfidentialClientApplication
import os

TENANT_ID = os.getenv("AZURE_TENANT_ID")
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
SCOPE = ["https://graph.microsoft.com/.default"]

def get_access_token():
    app = ConfidentialClientApplication(
        CLIENT_ID,
        authority=f"https://login.microsoftonline.com/{TENANT_ID}",
        client_credential=CLIENT_SECRET
    )
    result = app.acquire_token_silent(SCOPE, account=None)
    if not result:
        result = app.acquire_token_for_client(scopes=SCOPE)
    return result.get("access_token")

Fetching Emails

import requests

def get_unread_emails(user_email: str, top: int = 50) -> list[dict]:
    token = get_access_token()
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    url = f"https://graph.microsoft.com/v1.0/users/{user_email}/messages"
    params = {
        "$filter": "isRead eq false",
        "$top": top,
        "$select": "id,subject,from,receivedDateTime,body,conversationId,importance",
        "$orderby": "receivedDateTime desc"
    }

    response = requests.get(url, headers=headers, params=params)
    messages = response.json().get("value", [])

    return [{
        "id": m["id"],
        "subject": m["subject"],
        "from": m["from"]["emailAddress"]["address"],
        "from_name": m["from"]["emailAddress"]["name"],
        "received": m["receivedDateTime"],
        "body": m["body"]["content"],
        "conversation_id": m["conversationId"]
    } for m in messages]

Automated Email Triage

Triage is where the volume problem lives. Most inboxes receive 100+ emails per day; a senior executive might get 300. Claude can score every incoming email for urgency, action type, and routing destination in under a second โ€” and do it with the contextual understanding that keyword-based filters completely lack.

import anthropic
import json

client = anthropic.Anthropic()

TRIAGE_SYSTEM = """You are an expert executive assistant triaging emails for a senior technology executive.

For each email, return a JSON object with:
- priority: "urgent" | "high" | "normal" | "low" | "auto-archive"
- category: "client" | "internal" | "vendor" | "recruitment" | "newsletter" | "automated" | "legal" | "finance"
- action_required: boolean
- action_type: "reply" | "delegate" | "review" | "sign" | "approve" | null
- delegate_to: suggested team member or role, or null
- summary: 1-sentence summary of what the email is about
- urgency_reason: brief explanation if urgent, null otherwise

Rules:
- Emails from clients or partners about active projects are always high or urgent
- Emails requiring a signature or financial approval are urgent
- Legal notices are always urgent
- Automated notifications from monitoring tools are auto-archive unless they contain ERROR or CRITICAL
- Marketing/newsletter emails are auto-archive
- Recruitment emails from external recruiters are low priority"""

def triage_email(email: dict) -> dict:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        system=TRIAGE_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"""From: {email['from_name']} <{email['from']}>
Subject: {email['subject']}
Received: {email['received']}

{email['body'][:2000]}"""
        }]
    )

    try:
        return json.loads(response.content[0].text)
    except json.JSONDecodeError:
        # Claude occasionally wraps JSON in markdown โ€” strip it
        text = response.content[0].text
        start = text.find('{')
        end = text.rfind('}') + 1
        return json.loads(text[start:end])

def apply_triage_labels(user_email: str, email_id: str, triage: dict):
    """Apply Microsoft Graph categories and flags based on triage output."""
    token = get_access_token()
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    update_data = {}

    if triage["priority"] == "urgent":
        update_data["importance"] = "high"
        update_data["flag"] = {"flagStatus": "flagged"}
    elif triage["priority"] == "auto-archive":
        update_data["isRead"] = True
        # Move to archive folder handled separately

    if update_data:
        requests.patch(
            f"https://graph.microsoft.com/v1.0/users/{user_email}/messages/{email_id}",
            headers=headers,
            json=update_data
        )

Batch Processing for High-Volume Inboxes

For executives receiving 200+ emails per day, batch triaging is more efficient than one-at-a-time processing. Send 20 emails per Claude call with a structured prompt asking for a JSON array of triage results. This reduces latency from 200 API calls to 10, and with prompt caching on the system prompt, you'll see 70% lower token costs. Use Claude's batch API for asynchronous processing if real-time triage isn't required.

Smart Reply Drafting

The highest-value use case in the Claude email assistant isn't triage โ€” it's reply drafting. A well-crafted draft reply that captures the right tone, references the right context, and includes the right information saves 10โ€“15 minutes per email on complex correspondence. At 20 such emails per day, that's 3+ hours recovered daily.

Want a production Claude email assistant for your team?

We build enterprise email AI with full Microsoft 365 and Gmail integration, CRM sync, and governance controls. Deployed and maintained by Claude Certified Architects.

Book a Free Strategy Call โ†’

Context-Aware Reply Generation

def draft_reply(email: dict, thread_context: list[dict], user_context: dict) -> str:
    """Generate a contextual draft reply to an email."""

    thread_summary = "\n".join([
        f"[{m['from_name']} on {m['received'][:10]}]: {m['body'][:300]}"
        for m in thread_context[-5:]  # Last 5 messages in thread
    ])

    system_prompt = f"""You are drafting a professional email reply for {user_context['name']},
{user_context['title']} at {user_context['company']}.

Writing style: {user_context.get('style', 'professional, concise, direct')}
Signature: Will be added automatically โ€” do not include it.

Draft a reply that:
1. Addresses all questions and action items in the email
2. Matches the tone and formality of the conversation
3. Is concise โ€” senior executives do not write essays
4. Does not use filler phrases like "I hope this email finds you well"
5. Ends with a clear next step or action if one is needed

Return ONLY the email body text. No subject line, no greeting prefix."""

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1000,
        system=system_prompt,
        messages=[{
            "role": "user",
            "content": f"""Thread history:
{thread_summary}

Latest email to reply to:
From: {email['from_name']}
Subject: {email['subject']}

{email['body'][:3000]}"""
        }]
    )

    return response.content[0].text

The key to useful reply drafts is the user context object. Build a profile for each user that includes their communication style, common phrases they use, their role and title, and the names of their frequent correspondents. Store this in your application database and include it in every drafting prompt. After two weeks of use, Claude drafts in a voice that's nearly indistinguishable from the user's own โ€” which means editing time drops to seconds rather than minutes.

Thread Summarisation

Long email threads are one of the biggest productivity drains in enterprise communication. A 50-message thread about a contract negotiation, a project delivery dispute, or a product requirements discussion contains critical decisions buried in noise. Claude can summarise any thread to the decisions made, open questions, and required actions โ€” in under 10 seconds.

def summarise_thread(thread_messages: list[dict]) -> dict:
    """Summarise a complete email thread into structured output."""

    thread_text = "\n\n---\n\n".join([
        f"From: {m['from_name']} ({m['from']})\nDate: {m['received'][:10]}\n\n{m['body'][:1500]}"
        for m in sorted(thread_messages, key=lambda x: x['received'])
    ])

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        messages=[{
            "role": "user",
            "content": f"""Summarise this email thread. Return a JSON object with:
- topic: one-sentence description of what this thread is about
- key_decisions: list of decisions that were made (with who made them)
- open_questions: list of unresolved questions or pending items
- action_items: list of {{owner, action, due_date}} objects
- participants: list of {{name, email, role_in_thread}} objects
- sentiment: "positive" | "neutral" | "tense" | "unresolved"
- my_action_required: boolean โ€” does the thread require a response or action from me?

Thread:

{thread_text}"""
        }]
    )

    try:
        text = response.content[0].text
        start = text.find('{')
        end = text.rfind('}') + 1
        return json.loads(text[start:end])
    except:
        return {"topic": "Unable to parse", "raw": response.content[0].text}

CRM Synchronisation

Sales and account teams lose significant revenue because important client emails never make it into the CRM. Claude can automatically identify which emails are client-related, extract the relevant information (deal stage updates, commitments made, next steps agreed), and log them to Salesforce or HubSpot. This is one of the highest-ROI Claude integrations we've deployed across our client base โ€” teams recover 30โ€“45 minutes of manual CRM data entry per rep per day.

def extract_crm_data(email: dict, triage: dict) -> dict | None:
    """Extract CRM-relevant data from client emails."""

    if triage["category"] not in ["client", "vendor"]:
        return None

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"""Extract CRM data from this email. Return JSON or null if no CRM data is relevant.

{{
  "contact_email": email sender address,
  "contact_name": sender name,
  "account_name": company name if identifiable,
  "activity_type": "email_received" | "email_sent",
  "subject": email subject,
  "notes": key points for CRM notes (2-3 sentences max),
  "opportunity_signals": any mentions of deal size, timeline, decision criteria,
  "next_step": any commitment or next step mentioned,
  "sentiment": "positive" | "neutral" | "negative"
}}

Email:
From: {email['from_name']} <{email['from']}>
Subject: {email['subject']}

{email['body'][:2000]}"""
        }]
    )

    try:
        text = response.content[0].text
        if text.strip().lower() == "null":
            return None
        start = text.find('{')
        end = text.rfind('}') + 1
        return json.loads(text[start:end])
    except:
        return None

Connect this to your CRM's API โ€” Salesforce's REST API, HubSpot's contacts and activities endpoints, or Pipedrive's API all work well. Log each email as an activity on the relevant contact, and use Claude's extracted notes as the activity description. Sales managers get full visibility into client communication without chasing reps for CRM updates.

Enterprise Security for Email AI

Email is the most sensitive communication channel in most organisations. Before deploying a Claude email assistant at scale, you need answers to four questions: where does email content go when it's sent to Claude? who can access the processed outputs? how are drafts and summaries stored? and what happens in a data breach?

On data routing: use Anthropic's API with data residency commitments in place. Email content sent to the Claude API is not used for training under Anthropic's enterprise API terms. Review your data processing agreement. For organisations with strict data residency requirements, deploy via AWS Bedrock or Google Cloud Vertex AI โ€” Claude is available on both โ€” so email content never leaves your cloud environment.

On access controls: the service account authenticating to Microsoft Graph or Gmail should have delegated access scoped to specific mailboxes, not organization-wide. Implement a whitelist of mailboxes the assistant can access, reviewed quarterly. Store processed outputs (drafts, summaries, triage results) in your own database with row-level security. Our Claude security and governance service includes a full data flow audit and access control design for these deployments.

Ready to deploy a Claude email assistant across your executive team? Book a free strategy call โ€” we've deployed this pattern across financial services, legal, and consulting organisations where email security requirements are non-negotiable.

Related Implementation Guides

CI

ClaudeImplementation Team

Claude Certified Architects deploying AI across financial services, legal, healthcare, and SaaS. About us โ†’