iris

Iris — Rainbow Messenger. Reads your Gmail inbox, scores every email by urgency and sender importance, drafts replies for the top 5, and produces a daily action list. Saves 45+ minutes per day. Works with any Gmail account via app password — no OAuth dance required.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "iris" with this command: npx skills add occupythemilkyway/iris

🌈 Iris — Inbox Intelligence v1.0

⚡ Quick Start

  1. Go to myaccount.google.com/apppasswords → generate an app password for "Mail"
  2. Set GMAIL_ADDRESS and GMAIL_APP_PASSWORD
  3. Run — get your inbox scored, prioritised, and reply drafts ready in under 2 minutes

What you get:

  • Every email scored 0–100 by urgency, sender importance, and content signals
  • Attachment 📎 and meeting 📅 flags — so action items are never missed
  • Fast 2-pass fetch: headers for all emails, full body only for top 5 (handles large inboxes)
  • Colour-coded priority table: 🔴 Act Now · 🟠 Today · 🟡 This Week · ⚪ Archive
  • Context-aware reply drafts for your top 5 emails based on your name/role
  • Unsubscribe candidates: emails from lists you never open
  • inbox_report_[DATE].md with everything organised

🔒 Security

Connects to: imap.gmail.com (your credentials, read-only) · No external services App passwords only scope to the specific app — revoking them in Google Account settings instantly cuts access.


Step 1 — Install

pip3 install rich --break-system-packages --quiet

Step 2 — Triage your inbox

import os, imaplib, email, re
from email.header import decode_header
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box

console = Console()

GMAIL_ADDR = os.environ.get("GMAIL_ADDRESS", "")
GMAIL_PASS = os.environ.get("GMAIL_APP_PASSWORD", "")
try:
    SCAN_COUNT = int(os.environ.get("SCAN_COUNT", "50"))
except ValueError:
    console.print("[yellow]⚠️  SCAN_COUNT must be a whole number — defaulting to 50[/yellow]")
    SCAN_COUNT = 50
VIP_RAW    = os.environ.get("VIP_SENDERS", "")
VIP_LIST   = [v.strip().lower() for v in VIP_RAW.split(",") if v.strip()]
YOUR_NAME  = os.environ.get("YOUR_NAME", "")
YOUR_ROLE  = os.environ.get("YOUR_ROLE", "")

if not GMAIL_ADDR or not GMAIL_PASS:
    console.print(Panel(
        "[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are required.[/red]\n\n"
        "Create an app password at: [bold]myaccount.google.com/apppasswords[/bold]\n"
        "Choose: Mail → Your computer",
        title="Setup Required", border_style="red"))
    raise SystemExit(1)

URGENT_KEYWORDS = ["urgent","asap","deadline","immediately","action required","time sensitive",
                   "overdue","past due","invoice","payment","legal","lawsuit","critical","emergency"]
REPLY_KEYWORDS  = ["?","question","can you","could you","please","request","following up","reminder"]
NOISE_PATTERNS  = [r"unsubscribe",r"newsletter",r"no-reply",r"noreply",r"marketing@",
                   r"notifications?@",r"donotreply",r"info@.*\.(com|net)"]

def decode_str(s):
    if not s:
        return ""
    parts = decode_header(s)
    result = ""
    for part, enc in parts:
        if isinstance(part, bytes):
            try: result += part.decode(enc or "utf-8", errors="replace")
            except Exception: result += part.decode("latin-1", errors="replace")
        else:
            result += str(part)
    return result.strip()

MEETING_KEYWORDS = ["calendar invite","meeting request","zoom","teams meeting","google meet",
                    "webex","invited you","join the meeting","scheduled a","let's meet","video call"]

def score_email(subject, sender, body_snippet, age_hours, has_attachment=False):
    score = 50
    subj_lower   = subject.lower()
    body_lower   = body_snippet.lower()
    sender_lower = sender.lower()

    # Noise penalty applied first — VIPs can still overcome it
    is_noise = any(re.search(p, sender_lower) for p in NOISE_PATTERNS)
    if is_noise:
        score -= 40

    # VIP sender
    if any(vip in sender_lower for vip in VIP_LIST):
        score += 40

    # Urgency keywords in subject
    for kw in URGENT_KEYWORDS:
        if kw in subj_lower:
            score += 15
            break

    # Reply signals
    if any(kw in subj_lower or kw in body_lower for kw in REPLY_KEYWORDS):
        score += 10

    # Meeting / calendar invite
    if any(kw in subj_lower or kw in body_lower for kw in MEETING_KEYWORDS):
        score += 20

    # Has attachment — likely needs action
    if has_attachment:
        score += 15

    # Freshness
    if age_hours < 2:   score += 20
    elif age_hours < 8: score += 10
    elif age_hours > 48: score -= 10

    # RE: or FWD: (thread, likely needs response)
    if re.match(r"^(re:|fwd?:)", subj_lower):
        score += 5

    return max(0, min(100, score))

def get_priority(score):
    if score >= 80: return ("🔴", "Act Now",   "red")
    if score >= 60: return ("🟠", "Today",     "orange3")
    if score >= 40: return ("🟡", "This Week", "yellow")
    return             ("⚪", "Archive",    "dim")

console.print(Panel("[bold cyan]📧 🌈 Iris — Inbox Intelligence v1.0[/bold cyan]\nConnecting to Gmail...", border_style="cyan"))

try:
    mail = imaplib.IMAP4_SSL("imap.gmail.com")
    mail.login(GMAIL_ADDR, GMAIL_PASS)
    mail.select("inbox")
except Exception as e:
    console.print(Panel(f"[red]IMAP login failed: {e}[/red]\nCheck GMAIL_ADDRESS and GMAIL_APP_PASSWORD.", title="Login Error", border_style="red"))
    raise SystemExit(1)

_, msg_ids = mail.search(None, "ALL")
all_ids = msg_ids[0].split() if msg_ids and msg_ids[0] else []
if not all_ids:
    console.print(Panel("[yellow]Inbox is empty — nothing to triage.[/yellow]", border_style="yellow"))
    raise SystemExit(0)
recent_ids = all_ids[-SCAN_COUNT:] if len(all_ids) > SCAN_COUNT else all_ids
recent_ids = list(reversed(recent_ids))

console.print(f"[dim]Scanning {len(recent_ids)} emails...[/dim]")

# Pass 1: fetch headers only (fast) — score every email
emails_data = []
for eid in recent_ids:
    try:
        _, data = mail.fetch(eid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
        raw_header = data[0][1]
        msg = email.message_from_bytes(raw_header)

        subject  = decode_str(msg.get("Subject", "(no subject)"))
        sender   = decode_str(msg.get("From", ""))
        date_hdr = msg.get("Date", "")
        has_attachment = False  # unknown at header stage; updated in pass 2 for top5

        try:
            dt    = parsedate_to_datetime(date_hdr)
            age_h = (datetime.now(timezone.utc) - dt.astimezone(timezone.utc)).total_seconds() / 3600
        except Exception:
            age_h = 24

        score = score_email(subject, sender, "", age_h, has_attachment)
        icon, priority, color = get_priority(score)
        emails_data.append({
            "eid": eid, "subject": subject[:80], "sender": sender[:50],
            "score": score, "priority": priority, "icon": icon, "color": color,
            "age_hours": round(age_h, 1), "snippet": "", "is_noise": score < 20,
        })
    except Exception:
        continue

emails_data.sort(key=lambda x: x["score"], reverse=True)

# Pass 2: fetch full RFC822 only for top 5 non-noise — get body + attachments for drafts
top5_ids = [e["eid"] for e in emails_data if not e["is_noise"]][:5]
eid_to_idx = {e["eid"]: i for i, e in enumerate(emails_data)}
for eid in top5_ids:
    try:
        _, data = mail.fetch(eid, "(RFC822)")
        raw = data[0][1]
        msg = email.message_from_bytes(raw)
        body_snippet = ""
        has_attachment = False
        for part in msg.walk():
            ct = part.get_content_type()
            cd = str(part.get("Content-Disposition", ""))
            if "attachment" in cd:
                has_attachment = True
            if ct == "text/plain" and "attachment" not in cd and not body_snippet:
                try:
                    body_snippet = part.get_payload(decode=True).decode("utf-8", errors="replace")[:300]
                except Exception:
                    pass
        idx = eid_to_idx.get(eid)
        if idx is not None:
            e = emails_data[idx]
            e["snippet"] = body_snippet[:150]
            e["has_attachment"] = has_attachment
            # Re-score with body + attachment info
            new_score = score_email(e["subject"], e["sender"], body_snippet, e["age_hours"], has_attachment)
            e["score"] = new_score
            e["icon"], e["priority"], e["color"] = get_priority(new_score)
            e["is_noise"] = new_score < 20
    except Exception:
        continue

mail.logout()
emails_data.sort(key=lambda x: x["score"], reverse=True)

# Rich table
table = Table(title=f"📧 🌈 Iris — Inbox Intelligence — {len(emails_data)} emails scanned", box=box.ROUNDED, border_style="cyan")
table.add_column("Priority", width=12)
table.add_column("Score", width=6, justify="center")
table.add_column("Subject", width=45)
table.add_column("From", width=28)
table.add_column("Age", width=6, justify="center")
table.add_column("Flags", width=6)
for e in emails_data[:20]:
    flags = ""
    if e.get("has_attachment"): flags += "📎"
    if any(k in e["subject"].lower() or k in e["snippet"].lower() for k in ["zoom","meet","calendar invite","meeting"]): flags += "📅"
    table.add_row(
        f"[{e['color']}]{e['icon']} {e['priority']}[/{e['color']}]",
        str(e["score"]), e["subject"], e["sender"], f"{e['age_hours']:.0f}h", flags)
console.print(table)

# Draft replies for top 5 — contextual openers based on subject signals
top5 = [e for e in emails_data if not e["is_noise"]][:5]
drafts = []
URGENT_KW   = ["urgent","asap","deadline","overdue","invoice","payment","action required"]
QUESTION_KW = ["?","can you","could you","please advise","let me know","following up","reminder"]
for e in top5:
    greeting = "Hi there,"  # YOUR_NAME is for the signature, not the greeting
    role_line = f"\n{YOUR_ROLE}" if YOUR_ROLE else ""
    sig      = f"\n\nBest,\n{YOUR_NAME}{role_line}" if YOUR_NAME else ""
    subj_low = e["subject"].lower()
    snip_low = e["snippet"].lower()
    if any(k in subj_low for k in URGENT_KW):
        body = "Thank you for flagging this — I'll look into it right away and get back to you shortly."
    elif any(k in subj_low or k in snip_low for k in QUESTION_KW):
        body = "Thanks for your message. To answer your question: [your answer here]\n\nLet me know if you need anything else."
    elif "re:" in subj_low or "fwd:" in subj_low:
        body = "Thanks for the follow-up. Here's where things stand: [brief update]\n\nHappy to jump on a call if that's easier."
    else:
        body = "Thanks for reaching out. I've reviewed your message and [your response here]."
    draft = f"{greeting}\n\n{body}{sig}"
    drafts.append({"subject": e["subject"], "draft": draft})

noise = [e for e in emails_data if e["is_noise"]]

# Save report
date_str = datetime.now().strftime("%Y-%m-%d")
report_file = f"inbox_report_{date_str}.md"
with open(report_file, "w", encoding="utf-8") as f:
    f.write(f"# 🌈 Iris — Inbox Intelligence — {date_str}\n\n")
    f.write(f"**Scanned:** {len(emails_data)} emails  ·  **Act Now:** {len([e for e in emails_data if e['priority']=='Act Now'])}  ·  **Unsubscribe candidates:** {len(noise)}\n\n---\n\n")
    f.write("## 🔴 Action Required\n\n")
    for e in [x for x in emails_data if x["priority"] == "Act Now"][:10]:
        f.write(f"### {e['subject']}\n**From:** {e['sender']}  ·  **Score:** {e['score']}  ·  **Age:** {e['age_hours']:.0f}h\n\n{e['snippet']}\n\n---\n\n")
    f.write("## ✉️ Reply Drafts\n\n")
    if drafts:
        for d in drafts:
            f.write(f"### Re: {d['subject']}\n\n{d['draft']}\n\n---\n\n")
    else:
        f.write("_No reply drafts generated — top emails were all newsletters or noise. Check Unsubscribe Candidates below._\n\n")
    f.write("## 🗑️ Unsubscribe Candidates\n\n")
    for e in noise[:15]:
        f.write(f"- {e['sender']} — {e['subject']}\n")

act_now = len([e for e in emails_data if e["priority"] == "Act Now"])
console.print(Panel(
    f"[bold green]✅ Inbox triaged![/bold green]\n\n"
    f"[red bold]🔴 {act_now} Act Now[/red bold]\n"
    f"[orange3]{len([e for e in emails_data if e['priority']=='Today'])} Today[/orange3]\n"
    f"[yellow]{len([e for e in emails_data if e['priority']=='This Week'])} This Week[/yellow]\n"
    f"[dim]{len(noise)} Unsubscribe candidates[/dim]\n\n"
    f"📝 Full report: [cyan]{report_file}[/cyan]\n\n"
    f"[bold]What to do next:[/bold]\n"
    f"  1. Handle all 🔴 Act Now emails first\n"
    f"  2. Use the reply drafts as starting points\n"
    f"  3. Unsubscribe from the noise senders\n"
    f"  4. Run daily for a permanently clean inbox",
    title="📧 Triage Complete", border_style="green"))

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

Email Triage Pro

Intelligently categorize, prioritize, and draft replies for emails. Fetches emails via web_fetch (Gmail web) or browser, no OAuth required. AI-powered classi...

Registry SourceRecently Updated
2180Profile unavailable
Automation

Email Automation

Automate email triage, categorize, draft replies, and auto-archive in Gmail, Outlook, or IMAP to maintain an organized, efficient inbox.

Registry SourceRecently Updated
1K0Profile unavailable
Automation

Inbox Triage Bot

AI-powered email triage via IMAP (himalaya) or Google API. Fetches inbox, classifies messages by urgency, recommends actions, and generates daily markdown di...

Registry SourceRecently Updated
3730Profile unavailable
General

Email Assistant

多邮箱管理助手,支持 Gmail、163、QQ、Outlook、Hotmail。功能:(1) 读取收件箱并展示邮件摘要 (2) 关键词分析标记重要邮件 (3) 自动提取邮件中的日程信息并生成日历事件。适用于需要统一管理多个邮箱、避免错过重要邮件和日程的用户。

Registry SourceRecently Updated
1.2K0Profile unavailable