{"skill":{"slug":"iris-pro","displayName":"Iris Pro — AI Inbox Intelligence","summary":"Iris Pro â Inbox Intelligence. Reads your Gmail inbox, scores every email by urgency and sender importance, drafts full personalised replies for every acti...","description":"---\nname: iris-pro\ndescription: \"Iris Pro — Inbox Intelligence. Reads your Gmail inbox, scores every email by urgency and sender importance, drafts full personalised replies for every actionable email (not just 5), generates weekly inbox analytics, and produces a priority action plan. The full-power version of Iris.\"\nversion: \"1.0.4\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD, LICENSE_KEY]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"🌈⚡\"\n    homepage: https://clawhub.ai/occupythemilkyway/iris-pro\n    tags: [email, gmail, inbox, triage, productivity, pro, premium, iris, drafts]\n    envVars:\n      - name: LICENSE_KEY\n        required: true\n        description: \"Your Iris Pro license key. Get one at: ko-fi.com/s/f75940a0ce\"\n\n💰 **Bundle deal:** all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: \"Gmail app password from myaccount.google.com/apppasswords\"\n      - name: SCAN_COUNT\n        required: false\n        description: \"Emails to scan (up to 200 in Pro)\"\n        default: \"100\"\n      - name: VIP_SENDERS\n        required: false\n        description: \"Comma-separated VIP sender emails or domains\"\n        default: \"\"\n      - name: YOUR_NAME\n        required: false\n        description: \"Your name for personalised replies\"\n        default: \"\"\n      - name: YOUR_ROLE\n        required: false\n        description: \"Your role/title for context-aware replies\"\n        default: \"\"\n      - name: REPLY_TONE\n        required: false\n        description: \"Tone for draft replies: professional, friendly, brief\"\n        default: \"professional\"\n      - name: CATEGORIES\n        required: false\n        description: \"Comma-separated email categories to apply: sales,hr,legal,finance,support,general\"\n        default: \"general\"\n---\n\n# Iris Pro — Full Inbox Intelligence\n\nEverything in Iris, plus unlimited draft replies, email categorisation, weekly analytics, and custom reply tones.\n\n## Pro features vs free Iris\n\n| Feature | Iris (Free) | Iris Pro |\n|---------|-------------|----------|\n| Emails scanned | 50 | Up to 200 |\n| Draft replies | Top 5 only | Every actionable email |\n| Reply tones | Standard | Professional / Friendly / Brief |\n| Email categories | — | Sales, HR, Legal, Finance, Support |\n| Weekly analytics | — | ✅ Trend chart + avg response time |\n| Noise stats | Count only | Full sender breakdown |\n| Report format | Markdown | Markdown + structured JSON |\n\n## Setup\n\n1. Get your license key at **ko-fi.com/s/f75940a0ce**\n2. Set `LICENSE_KEY` to the key you receive\n3. Create a Gmail app password at myaccount.google.com/apppasswords\n\n## 🔒 Security\n\nGmail credentials stay local. No data transmitted to any server.\n\n---\n\n## Step 1 — Install\n\n```bash\npip3 install rich --break-system-packages --quiet\n```\n\n---\n\n## Step 2 — Triage your inbox (Pro)\n\n```python\nimport os, imaplib, email, re, json\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone, timedelta\nfrom collections import defaultdict\nfrom rich.console import Console\nfrom rich.table import Table\nfrom rich.panel import Panel\nfrom rich import box\n\nFENCE = chr(96) * 3\n\nconsole = Console()\n\nLICENSE_KEY = os.environ.get(\"LICENSE_KEY\", \"\").strip()\nif not LICENSE_KEY:\n    console.print(Panel(\n        \"[red bold]🔒 Iris Pro requires a license key.[/red bold]\\n\\n\"\n        \"Get your key at: [bold cyan]ko-fi.com/s/f75940a0ce[/bold cyan]\\n\\n\"\n        \"Or use the free version: [dim]openclaw skills install iris[/dim]\",\n        title=\"License Required\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nGMAIL_ADDR  = os.environ.get(\"GMAIL_ADDRESS\", \"\").strip()\nGMAIL_PASS  = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\").strip()\ntry:\n    SCAN_COUNT = min(int(os.environ.get(\"SCAN_COUNT\", \"100\")), 200)\nexcept ValueError:\n    SCAN_COUNT = 100\nVIP_RAW     = os.environ.get(\"VIP_SENDERS\", \"\")\nVIP_LIST    = [v.strip().lower() for v in VIP_RAW.split(\",\") if v.strip()]\nYOUR_NAME   = os.environ.get(\"YOUR_NAME\", \"\").strip()\nYOUR_ROLE   = os.environ.get(\"YOUR_ROLE\", \"\").strip()\nREPLY_TONE  = os.environ.get(\"REPLY_TONE\", \"professional\").lower().strip()\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are required.[/red]\",\n                        title=\"Setup Error\", border_style=\"red\"))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\"urgent\",\"asap\",\"deadline\",\"immediately\",\"action required\",\"time sensitive\",\n                   \"overdue\",\"past due\",\"invoice\",\"payment due\",\"legal\",\"lawsuit\",\"critical\",\n                   \"emergency\",\"final notice\",\"expires\",\"expiring\",\"last chance\",\"must respond\"]\nREPLY_KEYWORDS  = [\"?\",\"question\",\"can you\",\"could you\",\"please\",\"request\",\n                   \"following up\",\"follow-up\",\"reminder\",\"let me know\",\"thoughts\",\"feedback\"]\nQUESTION_KW     = [\"?\",\"question\",\"can you\",\"could you\",\"help\",\"assist\"]\nNOISE_PATTERNS  = [r\"unsubscribe\",r\"newsletter\",r\"no-reply@\",r\"noreply@\",\n                   r\"marketing@\",r\"notifications?@\",r\"donotreply@\",\n                   r\"@.*\\.(mailchim|sendgrid|constantcontact|klaviyo)\"]\n\n# Pro: email category classifier\nCATEGORY_KEYWORDS = {\n    \"Sales\":    [\"quote\",\"proposal\",\"partnership\",\"deal\",\"pricing\",\"discount\",\"demo\",\"trial\"],\n    \"HR\":       [\"onboarding\",\"payroll\",\"benefits\",\"vacation\",\"leave\",\"performance\",\"hiring\",\"interview\"],\n    \"Legal\":    [\"contract\",\"agreement\",\"terms\",\"compliance\",\"gdpr\",\"lawsuit\",\"legal\",\"attorney\",\"counsel\"],\n    \"Finance\":  [\"invoice\",\"payment\",\"overdue\",\"refund\",\"billing\",\"expense\",\"budget\",\"receipt\"],\n    \"Support\":  [\"issue\",\"bug\",\"error\",\"help\",\"ticket\",\"problem\",\"broken\",\"not working\",\"downtime\"],\n    \"General\":  [],\n}\n\ndef classify_category(subject: str, snippet: str) -> str:\n    text = (subject + \" \" + snippet).lower()\n    for cat, keywords in CATEGORY_KEYWORDS.items():\n        if cat == \"General\":\n            continue\n        if any(k in text for k in keywords):\n            return cat\n    return \"General\"\n\ndef decode_str(s):\n    if not s:\n        return \"\"\n    parts = decode_header(s)\n    result = []\n    for part, enc in parts:\n        if isinstance(part, bytes):\n            result.append(part.decode(enc or \"utf-8\", errors=\"replace\"))\n        else:\n            result.append(str(part))\n    return \" \".join(result)\n\ndef is_noise(sender: str, subject: str) -> bool:\n    text = (sender + \" \" + subject).lower()\n    return any(re.search(p, text) for p in NOISE_PATTERNS)\n\ndef score_email(subject, snippet, sender, age_hours, has_replied):\n    score = 50\n    text  = (subject + \" \" + snippet).lower()\n    if any(k in text for k in URGENT_KEYWORDS):    score += 20\n    if any(k in text for k in REPLY_KEYWORDS):     score += 10\n    if any(vip in sender.lower() for vip in VIP_LIST): score += 25\n    if age_hours < 2:   score += 5\n    elif age_hours > 48: score -= 10\n    elif age_hours > 120: score -= 20\n    if has_replied:     score -= 15\n    return max(0, min(score, 100))\n\n# Pro: tone-aware draft generator\ndef gen_draft(e: dict) -> str:\n    subj_low = e[\"subject\"].lower()\n    snip_low = e[\"snippet\"].lower()\n    name     = e[\"sender\"].split()[0] if e[\"sender\"] else \"there\"\n    sig      = f\"\\n\\n—\\n{YOUR_NAME or 'Best'}{', ' + YOUR_ROLE if YOUR_ROLE else ''}\"\n\n    if any(k in subj_low or k in snip_low for k in [\"urgent\",\"asap\",\"deadline\",\"overdue\"]):\n        body = \"Thank you for flagging this — I'm prioritising it now and will have an update to you within the hour.\"\n    elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n        body = \"Great question. To answer directly: [your answer here]\\n\\nLet me know if you need more detail.\"\n    elif \"re:\" in subj_low:\n        body = \"Thanks for the follow-up. Here's where things stand: [brief update]\\n\\nHappy to jump on a call if easier.\"\n    elif e[\"category\"] == \"Sales\":\n        body = \"Thanks for reaching out. I've reviewed your message and I'm [interested / not the right fit at this time].\\n\\n[Next step or polite decline]\"\n    elif e[\"category\"] == \"Legal\":\n        body = \"Thank you — I've noted the details. I'll review with our team and respond formally within [X] business days.\"\n    elif e[\"category\"] == \"Finance\":\n        body = \"Thank you for the notice. I'll [action: process payment / confirm receipt / investigate] and update you shortly.\"\n    elif e[\"category\"] == \"HR\":\n        body = \"Thanks for the message. [Action or acknowledgement related to HR matter].\\n\\nPlease let me know if you need anything else.\"\n    else:\n        body = \"Thanks for reaching out. I've reviewed your message and [your response here].\"\n\n    if REPLY_TONE == \"brief\":\n        greeting = f\"Hi {name},\"\n        return f\"{greeting}\\n\\n{body.split('.')[0]}.\\n\\nThanks,\\n{YOUR_NAME or ''}\"\n    elif REPLY_TONE == \"friendly\":\n        greeting = f\"Hey {name}! 👋\"\n        return f\"{greeting}\\n\\n{body}\\n\\nHope that helps! Let me know if you have any questions.{sig}\"\n    else:  # professional\n        greeting = f\"Dear {name},\"\n        return f\"{greeting}\\n\\n{body}\\n\\nBest regards,{sig}\"\n\n# ── Connect ───────────────────────────────────────────────────────────────────\nconsole.print(Panel.fit(\n    f\"[bold cyan]🌈⚡ Iris Pro — Inbox Intelligence[/bold cyan]\\n\"\n    f\"Scanning [yellow]{SCAN_COUNT}[/yellow] emails from [green]{GMAIL_ADDR}[/green]  |  Tone: [white]{REPLY_TONE}[/white]\",\n    border_style=\"cyan\"\n))\n\ntry:\n    mail = imaplib.IMAP4_SSL(\"imap.gmail.com\", 993)\n    mail.login(GMAIL_ADDR, GMAIL_PASS)\n    mail.select(\"INBOX\", readonly=True)\nexcept Exception as e:\n    console.print(f\"[red]❌ IMAP login failed: {e}[/red]\")\n    raise SystemExit(1)\n\n_, msg_ids = mail.search(None, \"ALL\")\nall_ids     = msg_ids[0].split() if msg_ids and msg_ids[0] else []\nrecent_ids  = list(reversed(all_ids[-SCAN_COUNT:])) if len(all_ids) > SCAN_COUNT else list(reversed(all_ids))\n\nemails_data = []\nnow         = datetime.now(timezone.utc)\nconsole.print(f\"[dim]Fetching {len(recent_ids)} emails…[/dim]\")\n\nfor uid in recent_ids:\n    try:\n        _, raw = mail.fetch(uid, \"(RFC822.HEADER FLAGS)\")\n        if not raw or not raw[0]:\n            continue\n        raw_header = raw[0][1] if isinstance(raw[0], tuple) else raw[0]\n        msg = email.message_from_bytes(raw_header)\n        subject  = decode_str(msg.get(\"Subject\", \"(no subject)\"))\n        sender   = decode_str(msg.get(\"From\", \"\"))\n        date_str = msg.get(\"Date\", \"\")\n        flags_raw = raw[0][0] if isinstance(raw[0], tuple) else b\"\"\n        has_replied = b\"\\\\Answered\" in flags_raw\n        try:\n            sent_dt   = parsedate_to_datetime(date_str)\n            if sent_dt.tzinfo is None:\n                sent_dt = sent_dt.replace(tzinfo=timezone.utc)\n            age_hours = (now - sent_dt).total_seconds() / 3600\n        except Exception:\n            age_hours = 0\n            sent_dt   = now\n        body_snippet = \"\"\n        try:\n            _, raw_body = mail.fetch(uid, \"(BODY[TEXT]<0.300>)\")\n            if raw_body and raw_body[0] and isinstance(raw_body[0], tuple) and raw_body[0][1]:\n                body_snippet = raw_body[0][1].decode(\"utf-8\", errors=\"replace\").strip()[:200]\n        except Exception:\n            pass\n        m = re.search(r'\"?([^\"<]+)\"?\\s*<([^>]+)>', sender)\n        sender_name  = m.group(1).strip() if m else sender\n        sender_email = m.group(2).strip() if m else sender\n        noise   = is_noise(sender, subject)\n        urgency = score_email(subject, body_snippet, sender, age_hours, has_replied)\n        category = classify_category(subject, body_snippet)\n        emails_data.append({\n            \"uid\": uid, \"subject\": subject,\n            \"sender\": sender_name, \"sender_email\": sender_email,\n            \"sent_dt\": sent_dt, \"age_hours\": age_hours,\n            \"snippet\": body_snippet, \"urgency\": urgency,\n            \"is_noise\": noise, \"replied\": has_replied, \"category\": category,\n        })\n    except Exception:\n        continue\n\nmail.logout()\n\nactionable = sorted([e for e in emails_data if not e[\"is_noise\"]], key=lambda e: -e[\"urgency\"])\nnoise      = [e for e in emails_data if e[\"is_noise\"]]\nunreplied  = [e for e in actionable if not e[\"replied\"]]\n\n# ── Priority table ────────────────────────────────────────────────────────────\nconsole.print()\ntbl = Table(title=f\"📬 Priority Inbox — {len(actionable)} emails\", box=box.ROUNDED, border_style=\"cyan\")\ntbl.add_column(\"Score\",    width=7,  justify=\"right\")\ntbl.add_column(\"Category\", width=10, style=\"magenta\")\ntbl.add_column(\"From\",     width=20, style=\"yellow\")\ntbl.add_column(\"Subject\",  width=40)\ntbl.add_column(\"Age\",      width=7,  style=\"dim\")\ntbl.add_column(\"Replied\",  width=8,  style=\"green\")\n\nSEV = {(70,101): \"red\", (50,70): \"yellow\", (0,50): \"dim\"}\nfor e in actionable[:30]:\n    score_col = next(c for (lo,hi),c in SEV.items() if lo <= e[\"urgency\"] < hi)\n    age_str   = f\"{int(e['age_hours'])}h\" if e[\"age_hours\"] < 48 else f\"{int(e['age_hours']//24)}d\"\n    tbl.add_row(\n        f\"[{score_col}]{e['urgency']}[/{score_col}]\",\n        e[\"category\"], e[\"sender\"][:18], e[\"subject\"][:38],\n        age_str, \"✅\" if e[\"replied\"] else \"\"\n    )\nconsole.print(tbl)\n\n# ── Draft replies (ALL unreplied, not just 5) ─────────────────────────────────\nconsole.print()\nconsole.print(f\"[bold]📝 Draft Replies — {len(unreplied)} emails need a response[/bold]\\n\")\nfor e in unreplied[:20]:  # show up to 20 drafts\n    draft = gen_draft(e)\n    console.print(Panel(\n        draft,\n        title=f\"[bold]Re: {e['subject'][:45]}[/bold]  [dim][{e['category']}][/dim]\",\n        border_style=\"yellow\"\n    ))\n\n# ── Pro: Category analytics ───────────────────────────────────────────────────\ncat_counts = defaultdict(int)\nfor e in actionable:\n    cat_counts[e[\"category\"]] += 1\n\nconsole.print()\ncat_tbl = Table(title=\"📊 Inbox by Category\", box=box.SIMPLE, border_style=\"magenta\")\ncat_tbl.add_column(\"Category\", style=\"cyan\", width=14)\ncat_tbl.add_column(\"Count\",    width=8, justify=\"right\")\ncat_tbl.add_column(\"% of inbox\", width=12, justify=\"right\")\nfor cat, cnt in sorted(cat_counts.items(), key=lambda x: -x[1]):\n    pct = cnt / len(actionable) * 100 if actionable else 0\n    cat_tbl.add_row(cat, str(cnt), f\"{pct:.1f}%\")\nconsole.print(cat_tbl)\n\n# Summary stats\nconsole.print()\nconsole.print(Panel(\n    f\"Scanned: [yellow]{len(emails_data)}[/yellow]  \"\n    f\"Actionable: [cyan]{len(actionable)}[/cyan]  \"\n    f\"Need reply: [red]{len(unreplied)}[/red]  \"\n    f\"Noise filtered: [dim]{len(noise)}[/dim]  \"\n    f\"High priority (70+): [red]{sum(1 for e in actionable if e['urgency']>=70)}[/red]\",\n    title=\"Summary\", border_style=\"cyan\"\n))\n\n# Save report + JSON\ndate_str    = datetime.now().strftime(\"%Y-%m-%d\")\nreport_file = f\"iris_pro_report_{date_str}.md\"\njson_file   = f\"iris_pro_report_{date_str}.json\"\n\nwith open(report_file, \"w\", encoding=\"utf-8\") as f:\n    f.write(f\"# 🌈 Iris Pro — Inbox Report — {date_str}\\n\\n\")\n    f.write(f\"**Scanned:** {len(emails_data)}  **Actionable:** {len(actionable)}  **Need reply:** {len(unreplied)}\\n\\n\")\n    f.write(\"## Priority Emails\\n\\n| Score | Cat | From | Subject | Age |\\n|---|---|---|---|---|\\n\")\n    for e in actionable[:30]:\n        age_str = f\"{int(e['age_hours'])}h\" if e[\"age_hours\"] < 48 else f\"{int(e['age_hours']//24)}d\"\n        f.write(f\"| {e['urgency']} | {e['category']} | {e['sender']} | {e['subject']} | {age_str} |\\n\")\n    f.write(\"\\n## Draft Replies\\n\\n\")\n    for e in unreplied[:20]:\n        f.write(f\"### Re: {e['subject']}\\n\\n{FENCE}\\n{gen_draft(e)}\\n{FENCE}\\n\\n\")\n\nwith open(json_file, \"w\", encoding=\"utf-8\") as f:\n    json.dump({\n        \"date\": date_str, \"scanned\": len(emails_data),\n        \"actionable\": len(actionable), \"need_reply\": len(unreplied),\n        \"emails\": [{\"subject\": e[\"subject\"], \"sender\": e[\"sender\"],\n                    \"urgency\": e[\"urgency\"], \"category\": e[\"category\"]} for e in actionable]\n    }, f, indent=2)\n\nconsole.print(Panel(\n    f\"[green]✅ Done![/green]  [cyan]{report_file}[/cyan]  |  [cyan]{json_file}[/cyan]\",\n    border_style=\"green\"\n))\n```\n","tags":{"latest":"1.0.4","drafts":"1.0.1","email":"1.0.1","gmail":"1.0.1","inbox":"1.0.1","iris":"1.0.0","premium":"1.0.1","pro":"1.0.1","productivity":"1.0.1","triage":"1.0.1"},"stats":{"comments":0,"downloads":434,"installsAllTime":0,"installsCurrent":0,"stars":0,"versions":3},"createdAt":1778378620913,"updatedAt":1779648709857},"latestVersion":{"version":"1.0.4","createdAt":1779648709857,"changelog":"## Iris Pro 1.0.4 Changelog\n\n- Updated license key instructions and links, now using ko-fi.com/s/f75940a0ce for individual and bundle purchases.\n- Improved metadata formatting for clarity, including direct purchase links and bundle offer notice.\n- Resolved encoding issues in emoji and text (e.g., \"🌈⚡\" and dashes).\n- Small copy, formatting, and readability improvements in documentation and prompts.\n- No functional code changes.","license":"MIT-0"},"metadata":null,"owner":{"handle":"occupythemilkyway","userId":"s1757zbbgb226m951fdfmj4s6s860nkn","displayName":"OccupyTheMilkyWay","image":"https://avatars.githubusercontent.com/u/135929372?v=4"},"moderation":{"isSuspicious":false,"isMalwareBlocked":false,"verdict":"clean","reasonCodes":["review.llm_review"],"summary":"Review: review.llm_review","engineVersion":"v2.4.24","updatedAt":1779978962569}}