Back to skills
SkillHub ClubAnalyze Data & AIFull StackData / AI

gmail-ai

AI-enhanced Gmail β€” smart email triage, priority scoring, AI-generated replies, thread summarization, and automated categorization. IMAP/SMTP with OpenRouter-powered intelligence. Use for inbox zero, email management, smart replies, and email automation.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
2,963
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
A92.0

Install command

npx @skill-hub/cli install openclaw-skills-gmail-ai

Repository

openclaw/skills

Skill path: skills/aiwithabidi/gmail-ai

AI-enhanced Gmail β€” smart email triage, priority scoring, AI-generated replies, thread summarization, and automated categorization. IMAP/SMTP with OpenRouter-powered intelligence. Use for inbox zero, email management, smart replies, and email automation.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Data / AI.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install gmail-ai into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding gmail-ai to shared team environments
  • Use gmail-ai for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: gmail-ai
description: AI-enhanced Gmail β€” smart email triage, priority scoring, AI-generated replies, thread summarization, and automated categorization. IMAP/SMTP with OpenRouter-powered intelligence. Use for inbox zero, email management, smart replies, and email automation.
homepage: https://www.agxntsix.ai
license: MIT
compatibility: Python 3.10+, Gmail App Password
metadata: {"openclaw": {"emoji": "\ud83d\udce7", "requires": {"env": ["GMAIL_APP_PASSWORD"]}, "primaryEnv": "GMAIL_APP_PASSWORD", "homepage": "https://www.agxntsix.ai"}}
---

# πŸ“§ Gmail AI

AI-enhanced Gmail for OpenClaw agents. Fork of gmail v1.0.6 with AI triage, priority scoring, smart replies, and email summarization.

## What's New vs gmail

- **AI triage** β€” auto-categorize emails (urgent/actionable/FYI/spam)
- **Priority scoring** β€” 0-100 score based on sender, subject, content
- **Smart replies** β€” context-aware reply generation in multiple tones
- **Email summarization** β€” TL;DR for long threads
- **Send email** β€” compose and send via SMTP

## Requirements

| Variable | Required | Description |
|----------|----------|-------------|
| `GMAIL_ADDRESS` | βœ… | Your Gmail address |
| `GMAIL_APP_PASSWORD` | βœ… | Gmail App Password ([create one](https://myaccount.google.com/apppasswords)) |
| `OPENROUTER_API_KEY` | Optional | For AI triage, replies, and summaries |

## Quick Start

```bash
# Fetch recent unread emails
python3 {baseDir}/scripts/gmail_ai.py inbox --unread --limit 10

# Fetch by label
python3 {baseDir}/scripts/gmail_ai.py inbox --label INBOX --limit 20

# Fetch from specific sender
python3 {baseDir}/scripts/gmail_ai.py inbox --from "[email protected]"

# AI triage β€” categorize emails
python3 {baseDir}/scripts/gmail_ai.py triage --limit 20

# Priority scoring
python3 {baseDir}/scripts/gmail_ai.py priority --limit 20

# Summarize an email thread
python3 {baseDir}/scripts/gmail_ai.py summarize <message_id>

# Generate smart reply
python3 {baseDir}/scripts/gmail_ai.py reply <message_id> --tone professional
python3 {baseDir}/scripts/gmail_ai.py reply <message_id> --tone friendly
python3 {baseDir}/scripts/gmail_ai.py reply <message_id> --tone brief

# Send email
python3 {baseDir}/scripts/gmail_ai.py send --to "[email protected]" --subject "Hello" --body "Message body"

# Send with CC/BCC
python3 {baseDir}/scripts/gmail_ai.py send --to "[email protected]" --cc "[email protected]" --subject "Hello" --body "Message"
```

## Commands

### `inbox`
Fetch emails from Gmail via IMAP.
- `--unread` β€” only unread messages
- `--label LABEL` β€” Gmail label/folder (default: INBOX)
- `--from ADDRESS` β€” filter by sender
- `--limit N` β€” max results (default: 10)
- `--since YYYY-MM-DD` β€” emails since date

### `triage`
AI-powered email categorization (requires `OPENROUTER_API_KEY`).
- Categories: πŸ”΄ Urgent, 🟑 Actionable, πŸ”΅ FYI, βšͺ Spam/Noise
- `--limit N` β€” number of emails to triage

### `priority`
Score emails 0-100 based on sender importance, subject urgency, and content.
- `--limit N` β€” number of emails to score
- Factors: known sender, urgency keywords, mentions of you, deadlines

### `summarize <message_id>`
Generate a TL;DR summary of an email or thread.

### `reply <message_id>`
Generate a context-aware reply draft.
- `--tone professional|friendly|brief|formal` β€” reply style
- `--context TEXT` β€” additional context for the reply

### `send`
Send email via SMTP.
- `--to ADDRESS` β€” recipient (required)
- `--subject TEXT` β€” subject line (required)
- `--body TEXT` β€” email body (required)
- `--cc ADDRESS` β€” CC recipient
- `--bcc ADDRESS` β€” BCC recipient

## Security Notes

- Uses Gmail App Passwords (not OAuth) β€” simpler setup, works with 2FA
- App Password is NOT your Google password
- Create one at: Google Account β†’ Security β†’ 2-Step Verification β†’ App Passwords
- IMAP must be enabled in Gmail settings

## Credits
Built by [M. Abidi](https://www.linkedin.com/in/mohammad-ali-abidi) | [agxntsix.ai](https://www.agxntsix.ai)
[YouTube](https://youtube.com/@aiwithabidi) | [GitHub](https://github.com/aiwithabidi)
Part of the **AgxntSix Skill Suite** for OpenClaw agents.

πŸ“… **Need help setting up OpenClaw for your business?** [Book a free consultation](https://cal.com/agxntsix/abidi-openclaw)


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "aiwithabidi",
  "slug": "gmail-ai",
  "displayName": "Gmail Ai",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1772614359588,
    "commit": "https://github.com/openclaw/skills/commit/07f889963d8c3a46c4d11c5a51c6af7b9c4697f0"
  },
  "history": []
}

```

### scripts/gmail_ai.py

```python
#!/usr/bin/env python3
"""AI-enhanced Gmail integration for OpenClaw agents."""

import argparse
import email
import email.utils
import imaplib
import json
import os
import smtplib
import sys
from email.header import decode_header
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart


IMAP_SERVER = "imap.gmail.com"
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587


def get_credentials():
    addr = os.environ.get("GMAIL_ADDRESS")
    pwd = os.environ.get("GMAIL_APP_PASSWORD")
    if not addr or not pwd:
        print("Error: GMAIL_ADDRESS and GMAIL_APP_PASSWORD must be set", file=sys.stderr)
        sys.exit(1)
    return addr, pwd


def llm_request(prompt, system="You are an email assistant."):
    key = os.environ.get("OPENROUTER_API_KEY")
    if not key:
        print("Error: OPENROUTER_API_KEY required for AI features", file=sys.stderr)
        sys.exit(1)
    from urllib.request import Request, urlopen
    from urllib.error import HTTPError
    data = json.dumps({
        "model": "anthropic/claude-haiku-4.5",
        "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}],
        "max_tokens": 2000,
    }).encode()
    req = Request("https://openrouter.ai/api/v1/chat/completions", data=data, headers={
        "Authorization": f"Bearer {key}", "Content-Type": "application/json",
    })
    try:
        with urlopen(req) as resp:
            return json.loads(resp.read().decode())["choices"][0]["message"]["content"]
    except HTTPError as e:
        print(f"LLM Error {e.code}: {e.read().decode()}", file=sys.stderr)
        sys.exit(1)


def decode_mime_header(header):
    if not header:
        return ""
    parts = decode_header(header)
    decoded = []
    for part, charset in parts:
        if isinstance(part, bytes):
            decoded.append(part.decode(charset or "utf-8", errors="replace"))
        else:
            decoded.append(part)
    return " ".join(decoded)


def get_email_body(msg):
    if msg.is_multipart():
        for part in msg.walk():
            ct = part.get_content_type()
            if ct == "text/plain":
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or "utf-8"
                    return payload.decode(charset, errors="replace")
        # Fallback to HTML
        for part in msg.walk():
            if part.get_content_type() == "text/html":
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or "utf-8"
                    return payload.decode(charset, errors="replace")
    else:
        payload = msg.get_payload(decode=True)
        if payload:
            charset = msg.get_content_charset() or "utf-8"
            return payload.decode(charset, errors="replace")
    return ""


def connect_imap():
    addr, pwd = get_credentials()
    try:
        imap = imaplib.IMAP4_SSL(IMAP_SERVER)
        imap.login(addr, pwd)
        return imap
    except imaplib.IMAP4.error as e:
        print(f"IMAP login failed: {e}", file=sys.stderr)
        sys.exit(1)


def fetch_emails(label="INBOX", unread=False, sender=None, since=None, limit=10):
    imap = connect_imap()
    imap.select(label, readonly=True)

    criteria = []
    if unread:
        criteria.append("UNSEEN")
    if sender:
        criteria.append(f'FROM "{sender}"')
    if since:
        from datetime import datetime
        dt = datetime.strptime(since, "%Y-%m-%d")
        criteria.append(f'SINCE {dt.strftime("%d-%b-%Y")}')
    if not criteria:
        criteria.append("ALL")

    search_str = " ".join(criteria)
    _, msg_nums = imap.search(None, search_str)
    ids = msg_nums[0].split()

    # Get most recent
    ids = ids[-limit:] if len(ids) > limit else ids
    ids.reverse()

    emails = []
    for mid in ids:
        _, msg_data = imap.fetch(mid, "(RFC822)")
        raw = msg_data[0][1]
        msg = email.message_from_bytes(raw)
        emails.append({
            "id": mid.decode(),
            "from": decode_mime_header(msg.get("From", "")),
            "to": decode_mime_header(msg.get("To", "")),
            "subject": decode_mime_header(msg.get("Subject", "")),
            "date": msg.get("Date", ""),
            "body": get_email_body(msg)[:2000],
            "message_id": msg.get("Message-ID", ""),
        })

    imap.logout()
    return emails


def cmd_inbox(args):
    emails = fetch_emails(
        label=args.label, unread=args.unread, sender=getattr(args, "from", None),
        since=args.since, limit=args.limit,
    )
    if not emails:
        print("  No emails found.")
        return
    for e in emails:
        snippet = e["body"][:100].replace("\n", " ").strip()
        print(f"  [{e['id']}] {e['date']}")
        print(f"    From: {e['from']}")
        print(f"    Subject: {e['subject']}")
        print(f"    Preview: {snippet}...")
        print()


def cmd_triage(args):
    emails = fetch_emails(unread=True, limit=args.limit)
    if not emails:
        print("  No emails to triage.")
        return

    email_summaries = []
    for e in emails:
        email_summaries.append({
            "id": e["id"], "from": e["from"], "subject": e["subject"],
            "preview": e["body"][:500],
        })

    prompt = f"""Categorize each email into one of these categories:
πŸ”΄ URGENT β€” needs immediate action (deadlines, emergencies, important people)
🟑 ACTIONABLE β€” requires a response but not urgent
πŸ”΅ FYI β€” informational, no action needed
βšͺ NOISE β€” newsletters, spam, automated notifications

For each email, output: [category emoji] Subject β€” brief reason

Emails:
{json.dumps(email_summaries, indent=2)}"""

    result = llm_request(prompt)
    print("  EMAIL TRIAGE")
    print(f"  {'='*50}")
    print(result)


def cmd_priority(args):
    emails = fetch_emails(unread=True, limit=args.limit)
    if not emails:
        print("  No emails to score.")
        return

    email_summaries = [{"id": e["id"], "from": e["from"], "subject": e["subject"],
                        "preview": e["body"][:500]} for e in emails]

    prompt = f"""Score each email from 0-100 for priority. Consider:
- Sender importance (known contacts > unknown, executives > automated)
- Subject urgency (deadline mentions, "urgent", "ASAP")
- Content: mentions of the recipient, action items, questions
- Time sensitivity: calendar invites, expiring offers

Output format per email:
[SCORE] Subject β€” from Sender β€” brief reason

Sort by score descending.

Emails:
{json.dumps(email_summaries, indent=2)}"""

    result = llm_request(prompt)
    print("  PRIORITY INBOX")
    print(f"  {'='*50}")
    print(result)


def cmd_summarize(args):
    emails = fetch_emails(limit=50)
    target = None
    for e in emails:
        if e["id"] == args.message_id:
            target = e
            break
    if not target:
        print(f"  Email {args.message_id} not found in recent messages.")
        return

    prompt = f"""Summarize this email concisely:

From: {target['from']}
Subject: {target['subject']}
Date: {target['date']}

Body:
{target['body'][:3000]}

Provide:
1. TL;DR (one sentence)
2. Key points (bullet list)
3. Action items (if any)
4. Response needed? (yes/no + why)"""

    result = llm_request(prompt)
    print(f"  SUMMARY: {target['subject']}")
    print(f"  {'='*50}")
    print(result)


def cmd_reply(args):
    emails = fetch_emails(limit=50)
    target = None
    for e in emails:
        if e["id"] == args.message_id:
            target = e
            break
    if not target:
        print(f"  Email {args.message_id} not found.")
        return

    context = f"\nAdditional context: {args.context}" if args.context else ""

    prompt = f"""Generate a reply to this email in a {args.tone} tone.

Original email:
From: {target['from']}
Subject: {target['subject']}
Body: {target['body'][:2000]}
{context}

Write ONLY the reply body (no subject line, no headers). Make it natural and appropriate for the tone requested."""

    result = llm_request(prompt)
    print(f"  DRAFT REPLY ({args.tone}) to: {target['subject']}")
    print(f"  {'='*50}")
    print(result)


def cmd_send(args):
    addr, pwd = get_credentials()
    msg = MIMEMultipart()
    msg["From"] = addr
    msg["To"] = args.to
    msg["Subject"] = args.subject
    if args.cc:
        msg["Cc"] = args.cc
    if args.bcc:
        msg["Bcc"] = args.bcc
    msg.attach(MIMEText(args.body, "plain"))

    recipients = [args.to]
    if args.cc:
        recipients.append(args.cc)
    if args.bcc:
        recipients.append(args.bcc)

    try:
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls()
            server.login(addr, pwd)
            server.sendmail(addr, recipients, msg.as_string())
        print(f"  Email sent to {args.to} | Subject: {args.subject}")
    except smtplib.SMTPException as e:
        print(f"SMTP Error: {e}", file=sys.stderr)
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(description="Gmail AI")
    sub = parser.add_subparsers(dest="command", required=True)

    p = sub.add_parser("inbox", help="Fetch emails")
    p.add_argument("--unread", action="store_true")
    p.add_argument("--label", default="INBOX")
    p.add_argument("--from", dest="from_addr")
    p.add_argument("--since")
    p.add_argument("--limit", type=int, default=10)

    p = sub.add_parser("triage", help="AI triage emails")
    p.add_argument("--limit", type=int, default=20)

    p = sub.add_parser("priority", help="Priority score emails")
    p.add_argument("--limit", type=int, default=20)

    p = sub.add_parser("summarize", help="Summarize an email")
    p.add_argument("message_id")

    p = sub.add_parser("reply", help="Generate smart reply")
    p.add_argument("message_id")
    p.add_argument("--tone", choices=["professional", "friendly", "brief", "formal"], default="professional")
    p.add_argument("--context")

    p = sub.add_parser("send", help="Send email")
    p.add_argument("--to", required=True)
    p.add_argument("--subject", required=True)
    p.add_argument("--body", required=True)
    p.add_argument("--cc")
    p.add_argument("--bcc")

    args = parser.parse_args()
    # Fix --from arg name
    if hasattr(args, "from_addr"):
        args.__dict__["from"] = args.from_addr

    cmds = {
        "inbox": cmd_inbox, "triage": cmd_triage, "priority": cmd_priority,
        "summarize": cmd_summarize, "reply": cmd_reply, "send": cmd_send,
    }
    cmds[args.command](args)


if __name__ == "__main__":
    main()

```

gmail-ai | SkillHub