Iris Lite — Inbox Intelligence (Free)

Iris Lite — Inbox Intelligence (Free). Scans your last 25 Gmail emails, shows urgency scores and a priority list, and drafts 2 quick replies. A free taste of what Iris Pro does for your full inbox every day.

Audits

Pass

Install

openclaw skills install iris-lite

Iris Lite — Free Inbox Triage

Scan your last 25 emails and get a quick priority list with 2 draft replies.

Free vs Pro

FeatureIris Lite (Free)Iris Pro
Emails scanned25Up to 200
Draft replies2Every actionable email
Email categories❌✅ Sales, HR, Legal, Finance
Reply tones❌Professional / Friendly / Brief
Weekly analytics❌✅
JSON export❌✅

👉 Upgrade: openclaw skills install iris-pro — get your key at ko-fi.com/occupythemilkyway


Step 1 — Install

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

Step 2 — Quick inbox scan (Lite)

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", "").strip()
GMAIL_PASS = os.environ.get("GMAIL_APP_PASSWORD", "").strip()
YOUR_NAME  = os.environ.get("YOUR_NAME", "").strip()

# Lite hard limits
SCAN_LIMIT   = 25
DRAFT_LIMIT  = 2

if not GMAIL_ADDR or not GMAIL_PASS:
    console.print(Panel(
        "[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are required.[/red]\n\n"
        "App password: myaccount.google.com/apppasswords",
        title="Setup Required", border_style="red"))
    raise SystemExit(1)

URGENT_KEYWORDS = ["urgent","asap","deadline","immediately","overdue","payment","invoice","emergency"]
NOISE_PATTERNS  = [r"unsubscribe",r"newsletter",r"no-reply@",r"noreply@",r"marketing@"]

def decode_str(s):
    if not s: return ""
    parts = decode_header(s)
    result = []
    for part, enc in parts:
        if isinstance(part, bytes):
            result.append(part.decode(enc or "utf-8", errors="replace"))
        else:
            result.append(str(part))
    return " ".join(result)

def is_noise(sender, subject):
    return any(re.search(p, (sender+subject).lower()) for p in NOISE_PATTERNS)

def score_email(subject, snippet, sender, age_hours, has_replied):
    score = 50
    text  = (subject + " " + snippet).lower()
    if any(k in text for k in URGENT_KEYWORDS): score += 20
    if "?" in text: score += 10
    if age_hours > 48: score -= 10
    if has_replied: score -= 15
    return max(0, min(score, 100))

console.print(Panel.fit(
    f"[bold cyan]🌈 Iris Lite — Quick Inbox Scan[/bold cyan]\n"
    f"Scanning last [yellow]{SCAN_LIMIT}[/yellow] emails from [green]{GMAIL_ADDR}[/green]\n"
    f"[dim]Lite: 25 emails, 2 drafts — upgrade to Pro for full inbox coverage[/dim]",
    border_style="cyan"
))

try:
    mail = imaplib.IMAP4_SSL("imap.gmail.com", 993)
    mail.login(GMAIL_ADDR, GMAIL_PASS)
    mail.select("INBOX", readonly=True)
except Exception as e:
    console.print(f"[red]❌ Login failed: {e}\nCheck GMAIL_ADDRESS and GMAIL_APP_PASSWORD.[/red]")
    raise SystemExit(1)

_, msg_ids = mail.search(None, "ALL")
all_ids    = msg_ids[0].split() if msg_ids and msg_ids[0] else []
recent_ids = list(reversed(all_ids[-SCAN_LIMIT:])) if len(all_ids) > SCAN_LIMIT else list(reversed(all_ids))

emails_data = []
now = datetime.now(timezone.utc)
for uid in recent_ids:
    try:
        _, raw = mail.fetch(uid, "(RFC822.HEADER FLAGS)")
        if not raw or not raw[0]: continue
        raw_header  = raw[0][1] if isinstance(raw[0], tuple) else raw[0]
        msg         = email.message_from_bytes(raw_header)
        subject     = decode_str(msg.get("Subject","(no subject)"))
        sender      = decode_str(msg.get("From",""))
        flags_raw   = raw[0][0] if isinstance(raw[0], tuple) else b""
        has_replied = b"\\Answered" in flags_raw
        try:
            sent_dt   = parsedate_to_datetime(msg.get("Date",""))
            if sent_dt.tzinfo is None: sent_dt = sent_dt.replace(tzinfo=timezone.utc)
            age_hours = (now - sent_dt).total_seconds() / 3600
        except Exception:
            age_hours = 0
        body_snippet = ""
        try:
            _, rb = mail.fetch(uid, "(BODY[TEXT]<0.150>)")
            if rb and rb[0] and isinstance(rb[0], tuple) and rb[0][1]:
                body_snippet = rb[0][1].decode("utf-8", errors="replace").strip()[:100]
        except Exception:
            pass
        m = re.search(r'"?([^"<]+)"?\s*<([^>]+)>', sender)
        sender_name = m.group(1).strip() if m else sender
        noise   = is_noise(sender, subject)
        urgency = score_email(subject, body_snippet, sender, age_hours, has_replied)
        emails_data.append({
            "subject": subject, "sender": sender_name, "age_hours": age_hours,
            "urgency": urgency, "is_noise": noise, "replied": has_replied, "snippet": body_snippet,
        })
    except Exception:
        continue

mail.logout()

actionable = sorted([e for e in emails_data if not e["is_noise"]], key=lambda e: -e["urgency"])
noise_count = sum(1 for e in emails_data if e["is_noise"])

console.print()
tbl = Table(title=f"📬 Priority Inbox — Top {min(len(actionable), 15)} of {len(actionable)}",
            box=box.ROUNDED, border_style="cyan")
tbl.add_column("Score", width=7, justify="right")
tbl.add_column("From",  width=22, style="yellow")
tbl.add_column("Subject", width=42)
tbl.add_column("Age",   width=6, style="dim")

for e in actionable[:15]:
    sc  = "red" if e["urgency"] >= 70 else "yellow" if e["urgency"] >= 50 else "dim"
    age = f"{int(e['age_hours'])}h" if e["age_hours"] < 48 else f"{int(e['age_hours']//24)}d"
    tbl.add_row(f"[{sc}]{e['urgency']}[/{sc}]", e["sender"][:20], e["subject"][:40], age)
console.print(tbl)

# 2 draft replies (Lite limit)
unreplied = [e for e in actionable if not e["replied"]]
console.print()
for e in unreplied[:DRAFT_LIMIT]:
    name   = e["sender"].split()[0]
    text   = (e["subject"] + " " + e["snippet"]).lower()
    if any(k in text for k in ["urgent","deadline","asap"]):
        body = "Thank you for flagging this — I'll look into it and get back to you shortly."
    elif "?" in text:
        body = "Thanks for your question. To answer: [your answer here]\n\nLet me know if you need more."
    else:
        body = "Thanks for reaching out. [Your response here]."
    sig  = f"\n\n— {YOUR_NAME}" if YOUR_NAME else ""
    console.print(Panel(
        f"Hi {name},\n\n{body}{sig}",
        title=f"[bold]📝 Draft — Re: {e['subject'][:45]}[/bold]",
        border_style="yellow"
    ))

console.print()
console.print(Panel(
    f"Scanned: [yellow]{len(emails_data)}[/yellow]  "
    f"Actionable: [cyan]{len(actionable)}[/cyan]  "
    f"Noise: [dim]{noise_count}[/dim]  "
    f"Drafts: [yellow]{min(len(unreplied), DRAFT_LIMIT)}/{DRAFT_LIMIT}[/yellow] (Lite limit)\n\n"
    f"[bold yellow]🔓 Want more?[/bold yellow]\n"
    f"Iris Pro scans [bold]200 emails[/bold], drafts [bold]every reply[/bold], classifies by category, "
    f"and gives you weekly analytics.\n\n"
    f"[bold cyan]openclaw skills install iris-pro[/bold cyan]\n"
    f"Get your key → [bold]ko-fi.com/occupythemilkyway[/bold]",
    title="Summary + Upgrade",
    border_style="cyan"
))