{"id":"a015aaca-4d4c-48d6-83a0-02202eb95be6","entityType":"agent","slug":"clawhub-occupythemilkyway-iris","name":"Iris — Rainbow Messenger","canonicalUrl":"https://www.xpersona.co/agent/clawhub-occupythemilkyway-iris","canonicalPath":"/agent/clawhub-occupythemilkyway-iris","generatedAt":"2026-10-12T16:56:47.166Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":null},"description":"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 act... Skill: Iris — Rainbow Messenger Owner: occupythemilkyway Summary: 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 act... Tags: email:1.0.1, gmail:1.0.1, inbox:1.0.1, latest:1.0.4, productivity:1.0.1, triage:1.0.1 Version history: v1.0.4 | 2026-05-24T18:48:44.956Z | user Version 1.0.4 - Fixed text encoding issues","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 959 downloads reported by the source. Last updated 10/12/2026.","installCommand":"clawhub skill install s1757zbbgb226m951fdfmj4s6s860nkn:iris","sourceUrl":"https://clawhub.ai/occupythemilkyway/iris","homepage":"https://clawhub.ai/occupythemilkyway/skills/iris","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/occupythemilkyway/iris","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/occupythemilkyway/skills/iris","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"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 act..."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":null},"stars":null,"forks":null,"downloads":959,"likes":null,"task":null,"library":null,"packageName":null,"latestVersion":"1.0.4","tractionLabel":"959 downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-12T01:50:17.563Z","emptyReason":null},"lastUpdatedAt":"2026-10-12T01:50:17.575Z","lastCrawledAt":"2026-10-12T01:50:17.563Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-13T01:50:17.563Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.4","createdAt":"2026-05-24T18:48:44.956Z","changelog":"Version 1.0.4 - Fixed text encoding issues in the skill description and documentation (all smart quotes, dashes, and emoji now rendered correctly). - Updated documentation to add \"Upgrade to Iris Pro\" instructions with purchase and installation details. - Bumped version from 1.0.1 to 1.0.4 in metadata. - No changes to source code; documentation and metadata updates only.","fileCount":3,"zipByteSize":7127},{"version":"1.0.1","createdAt":"2026-05-09T19:54:58.602Z","changelog":"Improvement: better IMAP error checklist, improved draft logic, cleaner urgency detection","fileCount":2,"zipByteSize":5793},{"version":"1.0.0","createdAt":"2026-05-05T06:26:39.481Z","changelog":"Iris 1.0.0 – Rainbow Messenger: Intelligent Gmail triage, reply drafting, and daily action planning. - Scores every Gmail message by urgency and sender importance; auto-flags attachments and meetings. - Drafts context-aware replies for your top 5 emails; produces a color-coded action report. - No OAuth required: works via Gmail app password—quick setup. - Supports custom VIP senders, customizable scan count, and context for drafts (name/role). - Easy-to-use, read-only connection; all results export to a tidy Markdown inbox report.","fileCount":2,"zipByteSize":5784}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1757zbbgb226m951fdfmj4s6s860nkn:iris","setupComplexity":"low","setupSteps":["Setup complexity is LOW. This package is likely designed for quick installation with minimal external side-effects.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-12T16:56:47.166Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-occupythemilkyway-iris/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":null},"readme":"Skill: Iris — Rainbow Messenger\n\nOwner: occupythemilkyway\n\nSummary: 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 act...\n\nTags: email:1.0.1, gmail:1.0.1, inbox:1.0.1, latest:1.0.4, productivity:1.0.1, triage:1.0.1\n\nVersion history:\n\nv1.0.4 | 2026-05-24T18:48:44.956Z | user\n\nVersion 1.0.4\n\n- Fixed text encoding issues in the skill description and documentation (all smart quotes, dashes, and emoji now rendered correctly).\n- Updated documentation to add \"Upgrade to Iris Pro\" instructions with purchase and installation details.\n- Bumped version from 1.0.1 to 1.0.4 in metadata.\n- No changes to source code; documentation and metadata updates only.\n\nv1.0.1 | 2026-05-09T19:54:58.602Z | user\n\nImprovement: better IMAP error checklist, improved draft logic, cleaner urgency detection\n\nv1.0.0 | 2026-05-05T06:26:39.481Z | user\n\nIris 1.0.0 – Rainbow Messenger: Intelligent Gmail triage, reply drafting, and daily action planning.\n\n- Scores every Gmail message by urgency and sender importance; auto-flags attachments and meetings.\n- Drafts context-aware replies for your top 5 emails; produces a color-coded action report.\n- No OAuth required: works via Gmail app password—quick setup.\n- Supports custom VIP senders, customizable scan count, and context for drafts (name/role).\n- Easy-to-use, read-only connection; all results export to a tidy Markdown inbox report.\n\nArchive index:\n\nArchive v1.0.4: 3 files, 7127 bytes\n\nFiles: skill-card.md (2236b), SKILL.md (16210b), _meta.json (123b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: iris\ndescription: \"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.\"\nversion: \"1.0.4\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"🌈📬\"\n    homepage: https://clawhub.ai/occupythemilkyway/iris\n    tags: [email, gmail, inbox, triage, productivity, iris, ai-drafts, time-saving]\n    envVars:\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address (e.g. you@gmail.com)\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: \"Gmail app password — create at myaccount.google.com/apppasswords (NOT your regular password). Choose: Mail → Other\"\n      - name: SCAN_COUNT\n        required: false\n        description: \"Number of recent emails to scan (default: 50)\"\n        default: \"50\"\n      - name: VIP_SENDERS\n        required: false\n        description: \"Comma-separated list of important sender email addresses or domains that should always score high\"\n        default: \"\"\n      - name: YOUR_NAME\n        required: false\n        description: \"Your name for personalising draft replies (e.g. Alex)\"\n        default: \"\"\n      - name: YOUR_ROLE\n        required: false\n        description: \"Your role for context-aware replies (e.g. Product Manager)\"\n        default: \"\"\n---\n\n# Iris — Inbox Intelligence\n\nRead your last 50 emails, score every one by urgency, get draft replies for your top 5, and see a clean action list — all in under 2 minutes.\n\n## What you get\n\n- **Urgency scoring** (0-100) based on keywords, sender importance, age, and reply signals\n- **Priority inbox**: top 5 emails that need action today, with one-click draft replies\n- **Noise filter**: newsletters, no-reply addresses, and marketing email suppressed automatically\n- **VIP tracking**: set specific senders or domains that always score high\n- **Daily report saved** to markdown — reference it any time during the day\n\n## Setup (one-time, 2 minutes)\n\n1. Go to **myaccount.google.com/apppasswords**\n2. Create an app password — select **Mail** → **Other (Custom name)**\n3. Copy the 16-character password\n4. Set `GMAIL_APP_PASSWORD` to that password (not your Google login password)\n\n## 🔒 Security\n\nConnects to: `imap.gmail.com` (your credentials, read-only). No external services contacted.\nApp passwords only scope to the specific app — revoke them instantly in Google Account settings.\n\n---\n\n## Step 1 — Install\n\n```bash\npip3 install rich --break-system-packages --quiet\n```\n\n---\n\n\n---\n\n## ⚡ Upgrade to Iris Pro\n\n👉 **Get Iris Pro** → **ko-fi.com/s/f75940a0ce** — $9 one-time\n\n```bash\nopenclaw skills install iris-pro\n# Set LICENSE_KEY env var to your key from Ko-fi, then run\n```\n\n💰 **Bundle deal:** all 5 Pro skills for **$29** → **ko-fi.com/s/7625accf3f** (save $16)\n\n## Step 2 — Triage your inbox\n\n```python\nimport os, imaplib, email, re\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\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\nGMAIL_ADDR = os.environ.get(\"GMAIL_ADDRESS\", \"\").strip()\nGMAIL_PASS = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\").strip()\ntry:\n    SCAN_COUNT = int(os.environ.get(\"SCAN_COUNT\", \"50\"))\nexcept ValueError:\n    console.print(\"[yellow]⚠️  SCAN_COUNT must be a whole number — defaulting to 50[/yellow]\")\n    SCAN_COUNT = 50\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()\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\n        \"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are both required.[/red]\\n\\n\"\n        \"How to create an app password:\\n\"\n        \"1. Go to [bold]myaccount.google.com/apppasswords[/bold]\\n\"\n        \"2. Select Mail → Other (Custom name)\\n\"\n        \"3. Copy the 16-character password\\n\"\n        \"4. Set GMAIL_APP_PASSWORD to that value\",\n        title=\"[bold red]⚙️  Setup Required[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\n    \"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\",\n]\nREPLY_KEYWORDS = [\n    \"?\", \"question\", \"can you\", \"could you\", \"please\", \"request\",\n    \"following up\", \"follow-up\", \"reminder\", \"let me know\", \"thoughts\",\n]\nQUESTION_KW = [\"?\", \"question\", \"can you\", \"could you\", \"help\", \"assist\"]\nNOISE_PATTERNS = [\n    r\"unsubscribe\", r\"newsletter\", r\"no-reply@\", r\"noreply@\",\n    r\"marketing@\", r\"notifications?@\", r\"donotreply@\",\n    r\"@.*\\.(mailchim|sendgrid|constantcontact|klaviyo)\",\n]\nNOISE_SUBJECTS = [\n    \"sale\", \"% off\", \"deal\", \"offer\", \"promo\", \"subscribe\", \"newsletter\",\n    \"weekly digest\", \"monthly update\", \"announcement\",\n]\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            try:\n                result.append(part.decode(enc or \"utf-8\", errors=\"replace\"))\n            except Exception:\n                result.append(part.decode(\"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    for pat in NOISE_PATTERNS:\n        if re.search(pat, text):\n            return True\n    for kw in NOISE_SUBJECTS:\n        if kw in subject.lower():\n            return True\n    return False\n\ndef score_email(subject: str, snippet: str, sender: str, age_hours: float, has_replied: bool) -> int:\n    score = 50\n    subj_low = subject.lower()\n    snip_low = snippet.lower()\n    # Urgency keywords\n    for kw in URGENT_KEYWORDS:\n        if kw in subj_low or kw in snip_low:\n            score += 20\n            break\n    # Reply needed\n    for kw in REPLY_KEYWORDS:\n        if kw in subj_low or kw in snip_low:\n            score += 10\n            break\n    # VIP sender\n    sender_low = sender.lower()\n    for vip in VIP_LIST:\n        if vip in sender_low:\n            score += 25\n            break\n    # Age penalty/boost\n    if age_hours < 2:\n        score += 5\n    elif age_hours < 24:\n        score += 0\n    elif age_hours > 48:\n        score -= 10\n    elif age_hours > 120:\n        score -= 20\n    # Has been replied to\n    if has_replied:\n        score -= 15\n    return max(0, min(score, 100))\n\n# ── Connect to Gmail ──────────────────────────────────────────────────────────\nconsole.print(Panel.fit(\n    f\"[bold cyan]🌈 Iris — Inbox Intelligence[/bold cyan]\\n\"\n    f\"Scanning [yellow]{SCAN_COUNT}[/yellow] recent emails from [green]{GMAIL_ADDR}[/green]\",\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 imaplib.IMAP4.error as e:\n    console.print(Panel(\n        f\"[red]IMAP login failed:[/red] {e}\\n\\n\"\n        \"Checklist:\\n\"\n        \"• Is GMAIL_ADDRESS correct? (full email, not just username)\\n\"\n        \"• Is GMAIL_APP_PASSWORD a 16-char app password (not your Google password)?\\n\"\n        \"• Did you enable IMAP in Gmail Settings → See all settings → Forwarding and POP/IMAP?\",\n        title=\"[bold red]Login Error[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\nexcept Exception as e:\n    console.print(f\"[red]Connection error: {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 []\nif not all_ids:\n    console.print(Panel(\"[yellow]Inbox is empty — nothing to triage.[/yellow]\", border_style=\"yellow\"))\n    raise SystemExit(0)\n\nrecent_ids = all_ids[-SCAN_COUNT:] if len(all_ids) > SCAN_COUNT else all_ids\nrecent_ids = list(reversed(recent_ids))  # newest first\n\n# ── Fetch and score emails ────────────────────────────────────────────────────\nemails_data = []\nnow = datetime.now(timezone.utc)\n\nconsole.print(f\"[dim]Fetching {len(recent_ids)} emails…[/dim]\")\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\n        # Parse date\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\n        # Fetch snippet\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):\n                raw_b = raw_body[0][1]\n                if raw_b:\n                    body_snippet = raw_b.decode(\"utf-8\", errors=\"replace\").strip()[:200]\n        except Exception:\n            pass\n\n        noise = is_noise(sender, subject)\n        urgency = score_email(subject, body_snippet, sender, age_hours, has_replied)\n\n        # Extract sender name and email\n        sender_match = re.search(r'\"?([^\"<]+)\"?\\s*<([^>]+)>', sender)\n        if sender_match:\n            sender_name  = sender_match.group(1).strip()\n            sender_email = sender_match.group(2).strip()\n        else:\n            sender_name  = sender\n            sender_email = sender\n\n        emails_data.append({\n            \"uid\":          uid,\n            \"subject\":      subject,\n            \"sender\":       sender_name,\n            \"sender_email\": sender_email,\n            \"age_hours\":    age_hours,\n            \"snippet\":      body_snippet,\n            \"urgency\":      urgency,\n            \"is_noise\":     noise,\n            \"replied\":      has_replied,\n        })\n    except Exception:\n        continue\n\nmail.logout()\n\n# ── Sort and filter ───────────────────────────────────────────────────────────\nactionable = [e for e in emails_data if not e[\"is_noise\"]]\nnoise      = [e for e in emails_data if e[\"is_noise\"]]\nactionable.sort(key=lambda e: -e[\"urgency\"])\n\n# ── Display: Priority inbox ───────────────────────────────────────────────────\nconsole.print()\npriority_table = Table(\n    title=f\"📬 Priority Inbox — {len(actionable)} actionable emails\",\n    box=box.ROUNDED, border_style=\"cyan\"\n)\npriority_table.add_column(\"Score\",   style=\"red\",    width=7,  justify=\"right\")\npriority_table.add_column(\"From\",    style=\"yellow\", width=22)\npriority_table.add_column(\"Subject\", style=\"white\",  width=40)\npriority_table.add_column(\"Age\",     style=\"dim\",    width=8)\npriority_table.add_column(\"Status\",  style=\"green\",  width=10)\n\nfor e in actionable[:20]:\n    age_str = (f\"{int(e['age_hours'])}h\" if e[\"age_hours\"] < 48\n               else f\"{int(e['age_hours'] // 24)}d\")\n    status  = \"✅ replied\" if e[\"replied\"] else \"\"\n    score_colour = \"red\" if e[\"urgency\"] >= 70 else \"yellow\" if e[\"urgency\"] >= 50 else \"dim\"\n    priority_table.add_row(\n        f\"[{score_colour}]{e['urgency']}[/{score_colour}]\",\n        e[\"sender\"][:20],\n        e[\"subject\"][:38],\n        age_str,\n        status,\n    )\n\nconsole.print(priority_table)\n\n# ── Display: Draft replies ────────────────────────────────────────────────────\nsig = f\"\\n\\n—\\n{YOUR_NAME or 'Best'}{', ' + YOUR_ROLE if YOUR_ROLE else ''}\"\ntop5 = [e for e in actionable if not e[\"replied\"]][:5]\n\nconsole.print()\nfor e in top5:\n    subj_low = e[\"subject\"].lower()\n    snip_low = e[\"snippet\"].lower()\n    greeting = f\"Hi {e['sender'].split()[0]},\"\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'll look into it right away and get back to you shortly.\"\n    elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n        body = \"Thanks for your message. To answer your question: [your answer here]\\n\\nLet me know if you need anything else.\"\n    elif \"re:\" in subj_low or \"fwd:\" 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 that's easier.\"\n    else:\n        body = \"Thanks for reaching out. I've reviewed your message and [your response here].\"\n    draft = f\"{greeting}\\n\\n{body}{sig}\"\n    console.print(Panel(\n        draft,\n        title=f\"[bold]📝 Draft: Re: {e['subject'][:45]}[/bold]  [dim](urgency: {e['urgency']})[/dim]\",\n        border_style=\"yellow\"\n    ))\n\n# ── Display: Stats ────────────────────────────────────────────────────────────\nconsole.print()\nconsole.print(Panel(\n    f\"📊 [bold]Summary[/bold]\\n\\n\"\n    f\"Scanned:    [yellow]{len(emails_data)}[/yellow] emails\\n\"\n    f\"Actionable: [cyan]{len(actionable)}[/cyan]  |  \"\n    f\"Noise: [dim]{len(noise)}[/dim]  |  \"\n    f\"Already replied: [green]{sum(1 for e in actionable if e['replied'])}[/green]\\n\"\n    f\"High priority (70+): [red]{sum(1 for e in actionable if e['urgency'] >= 70)}[/red]\",\n    border_style=\"cyan\"\n))\n\n# ── Save report ───────────────────────────────────────────────────────────────\ndate_str    = datetime.now().strftime(\"%Y-%m-%d\")\nreport_file = f\"inbox_report_{date_str}.md\"\nwith open(report_file, \"w\", encoding=\"utf-8\") as f:\n    f.write(f\"# 🌈 Iris — Inbox Report — {date_str}\\n\\n\")\n    f.write(f\"**Scanned:** {len(emails_data)}  **Actionable:** {len(actionable)}  **Noise:** {len(noise)}\\n\\n\")\n    f.write(\"## Priority Emails\\n\\n| Score | From | Subject | Age |\\n|-------|------|---------|-----|\\n\")\n    for e in actionable[:20]:\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['sender']} | {e['subject']} | {age_str} |\\n\")\n    f.write(\"\\n## Draft Replies\\n\\n\")\n    for e in top5:\n        subj_low = e[\"subject\"].lower()\n        snip_low = e[\"snippet\"].lower()\n        greeting = f\"Hi {e['sender'].split()[0]},\"\n        if any(k in subj_low or k in snip_low for k in [\"urgent\", \"asap\", \"deadline\"]):\n            body = \"Thank you for flagging this — I'll look into it right away.\"\n        elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n            body = \"Thanks for your message. To answer your question: [your answer here]\"\n        else:\n            body = \"Thanks for reaching out. [your response here]\"\n        f.write(f\"### Re: {e['subject']}\\n\\n{FENCE}\\n{greeting}\\n\\n{body}{sig}\\n{FENCE}\\n\\n\")\n\nconsole.print(Panel(\n    f\"[green]✅ Done![/green]  Report saved to [cyan]{report_file}[/cyan]\",\n    border_style=\"green\"\n))\n```\n\nFile v1.0.4:_meta.json\n\n{\n  \"ownerId\": \"kn7474wrtsjp988869sw7mygz1860n2v\",\n  \"slug\": \"iris\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1779648524956\n}\n\nFile v1.0.4:skill-card.md\n\n## Description:\n\nIris reads a Gmail inbox, scores recent email by urgency and sender importance, drafts replies for the top five actionable messages, and writes a daily action report.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[occupythemilkyway](https://clawhub.ai/user/occupythemilkyway)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nIndividuals and teams use Iris to triage recent Gmail messages, identify priority items, prepare draft replies, and keep a local markdown report for daily follow-up.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill handles Gmail credentials and email-derived content.\n\nMitigation: Use a Gmail app password, keep credentials in environment variables, and revoke the app password from Google Account settings when it is no longer needed.\n\nRisk: The skill writes sender, subject, urgency, and draft-reply data to a local markdown report.\n\nMitigation: Run it only in a trusted working directory and avoid regulated or highly sensitive mail unless report writing is changed or disabled.\n\nRisk: The documented install command uses system Python package installation.\n\nMitigation: Install and run the skill in a dedicated virtual environment.\n\n## Reference(s):\n\n- [Iris ClawHub skill page](https://clawhub.ai/occupythemilkyway/skills/iris)\n- [Iris ClawHub homepage](https://clawhub.ai/occupythemilkyway/iris)\n- [OccupyTheMilkyWay publisher profile](https://clawhub.ai/user/occupythemilkyway)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Console output and markdown report with draft-reply text and setup commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Writes a local inbox_report_<date>.md file containing email-derived sender, subject, priority, and draft-reply content.]\n\n## Skill Version(s):\n\n1.0.4 (source: frontmatter and server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.1: 2 files, 5793 bytes\n\nFiles: SKILL.md (17243b), _meta.json (123b)\n\nFile v1.0.1:SKILL.md\n\n---\nname: iris\ndescription: \"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.\"\nversion: \"1.0.1\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"ðð¬\"\n    homepage: https://clawhub.ai/occupythemilkyway/iris\n    tags: [email, gmail, inbox, triage, productivity, iris, ai-drafts, time-saving]\n    envVars:\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address (e.g. you@gmail.com)\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: \"Gmail app password â create at myaccount.google.com/apppasswords (NOT your regular password). Choose: Mail â Other\"\n      - name: SCAN_COUNT\n        required: false\n        description: \"Number of recent emails to scan (default: 50)\"\n        default: \"50\"\n      - name: VIP_SENDERS\n        required: false\n        description: \"Comma-separated list of important sender email addresses or domains that should always score high\"\n        default: \"\"\n      - name: YOUR_NAME\n        required: false\n        description: \"Your name for personalising draft replies (e.g. Alex)\"\n        default: \"\"\n      - name: YOUR_ROLE\n        required: false\n        description: \"Your role for context-aware replies (e.g. Product Manager)\"\n        default: \"\"\n---\n\n# Iris â Inbox Intelligence\n\nRead your last 50 emails, score every one by urgency, get draft replies for your top 5, and see a clean action list â all in under 2 minutes.\n\n## What you get\n\n- **Urgency scoring** (0-100) based on keywords, sender importance, age, and reply signals\n- **Priority inbox**: top 5 emails that need action today, with one-click draft replies\n- **Noise filter**: newsletters, no-reply addresses, and marketing email suppressed automatically\n- **VIP tracking**: set specific senders or domains that always score high\n- **Daily report saved** to markdown â reference it any time during the day\n\n## Setup (one-time, 2 minutes)\n\n1. Go to **myaccount.google.com/apppasswords**\n2. Create an app password â select **Mail** â **Other (Custom name)**\n3. Copy the 16-character password\n4. Set `GMAIL_APP_PASSWORD` to that password (not your Google login password)\n\n## ð Security\n\nConnects to: `imap.gmail.com` (your credentials, read-only). No external services contacted.\nApp passwords only scope to the specific app â revoke them instantly in Google Account settings.\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\n\n```python\nimport os, imaplib, email, re\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\nfrom rich.console import Console\nfrom rich.table import Table\nfrom rich.panel import Panel\nfrom rich import box\n\nconsole = Console()\n\nGMAIL_ADDR = os.environ.get(\"GMAIL_ADDRESS\", \"\").strip()\nGMAIL_PASS = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\").strip()\ntry:\n    SCAN_COUNT = int(os.environ.get(\"SCAN_COUNT\", \"50\"))\nexcept ValueError:\n    console.print(\"[yellow]â ï¸  SCAN_COUNT must be a whole number â defaulting to 50[/yellow]\")\n    SCAN_COUNT = 50\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()\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\n        \"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are both required.[/red]\\n\\n\"\n        \"How to create an app password:\\n\"\n        \"1. Go to [bold]myaccount.google.com/apppasswords[/bold]\\n\"\n        \"2. Select Mail â Other (Custom name)\\n\"\n        \"3. Copy the 16-character password\\n\"\n        \"4. Set GMAIL_APP_PASSWORD to that value\",\n        title=\"[bold red]âï¸  Setup Required[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\n    \"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\",\n]\nREPLY_KEYWORDS = [\n    \"?\", \"question\", \"can you\", \"could you\", \"please\", \"request\",\n    \"following up\", \"follow-up\", \"reminder\", \"let me know\", \"thoughts\",\n]\nQUESTION_KW = [\"?\", \"question\", \"can you\", \"could you\", \"help\", \"assist\"]\nNOISE_PATTERNS = [\n    r\"unsubscribe\", r\"newsletter\", r\"no-reply@\", r\"noreply@\",\n    r\"marketing@\", r\"notifications?@\", r\"donotreply@\",\n    r\"@.*\\.(mailchim|sendgrid|constantcontact|klaviyo)\",\n]\nNOISE_SUBJECTS = [\n    \"sale\", \"% off\", \"deal\", \"offer\", \"promo\", \"subscribe\", \"newsletter\",\n    \"weekly digest\", \"monthly update\", \"announcement\",\n]\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            try:\n                result.append(part.decode(enc or \"utf-8\", errors=\"replace\"))\n            except Exception:\n                result.append(part.decode(\"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    for pat in NOISE_PATTERNS:\n        if re.search(pat, text):\n            return True\n    for kw in NOISE_SUBJECTS:\n        if kw in subject.lower():\n            return True\n    return False\n\ndef score_email(subject: str, snippet: str, sender: str, age_hours: float, has_replied: bool) -> int:\n    score = 50\n    subj_low = subject.lower()\n    snip_low = snippet.lower()\n    # Urgency keywords\n    for kw in URGENT_KEYWORDS:\n        if kw in subj_low or kw in snip_low:\n            score += 20\n            break\n    # Reply needed\n    for kw in REPLY_KEYWORDS:\n        if kw in subj_low or kw in snip_low:\n            score += 10\n            break\n    # VIP sender\n    sender_low = sender.lower()\n    for vip in VIP_LIST:\n        if vip in sender_low:\n            score += 25\n            break\n    # Age penalty/boost\n    if age_hours < 2:\n        score += 5\n    elif age_hours < 24:\n        score += 0\n    elif age_hours > 48:\n        score -= 10\n    elif age_hours > 120:\n        score -= 20\n    # Has been replied to\n    if has_replied:\n        score -= 15\n    return max(0, min(score, 100))\n\n# ââ Connect to Gmail ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\nconsole.print(Panel.fit(\n    f\"[bold cyan]ð Iris â Inbox Intelligence[/bold cyan]\\n\"\n    f\"Scanning [yellow]{SCAN_COUNT}[/yellow] recent emails from [green]{GMAIL_ADDR}[/green]\",\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 imaplib.IMAP4.error as e:\n    console.print(Panel(\n        f\"[red]IMAP login failed:[/red] {e}\\n\\n\"\n        \"Checklist:\\n\"\n        \"â¢ Is GMAIL_ADDRESS correct? (full email, not just username)\\n\"\n        \"â¢ Is GMAIL_APP_PASSWORD a 16-char app password (not your Google password)?\\n\"\n        \"â¢ Did you enable IMAP in Gmail Settings â See all settings â Forwarding and POP/IMAP?\",\n        title=\"[bold red]Login Error[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\nexcept Exception as e:\n    console.print(f\"[red]Connection error: {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 []\nif not all_ids:\n    console.print(Panel(\"[yellow]Inbox is empty â nothing to triage.[/yellow]\", border_style=\"yellow\"))\n    raise SystemExit(0)\n\nrecent_ids = all_ids[-SCAN_COUNT:] if len(all_ids) > SCAN_COUNT else all_ids\nrecent_ids = list(reversed(recent_ids))  # newest first\n\n# ââ Fetch and score emails ââââââââââââââââââââââââââââââââââââââââââââââââââââ\nemails_data = []\nnow = datetime.now(timezone.utc)\n\nconsole.print(f\"[dim]Fetching {len(recent_ids)} emailsâ¦[/dim]\")\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\n        # Parse date\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\n        # Fetch snippet\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):\n                raw_b = raw_body[0][1]\n                if raw_b:\n                    body_snippet = raw_b.decode(\"utf-8\", errors=\"replace\").strip()[:200]\n        except Exception:\n            pass\n\n        noise = is_noise(sender, subject)\n        urgency = score_email(subject, body_snippet, sender, age_hours, has_replied)\n\n        # Extract sender name and email\n        sender_match = re.search(r'\"?([^\"<]+)\"?\\s*<([^>]+)>', sender)\n        if sender_match:\n            sender_name  = sender_match.group(1).strip()\n            sender_email = sender_match.group(2).strip()\n        else:\n            sender_name  = sender\n            sender_email = sender\n\n        emails_data.append({\n            \"uid\":          uid,\n            \"subject\":      subject,\n            \"sender\":       sender_name,\n            \"sender_email\": sender_email,\n            \"age_hours\":    age_hours,\n            \"snippet\":      body_snippet,\n            \"urgency\":      urgency,\n            \"is_noise\":     noise,\n            \"replied\":      has_replied,\n        })\n    except Exception:\n        continue\n\nmail.logout()\n\n# ââ Sort and filter âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\nactionable = [e for e in emails_data if not e[\"is_noise\"]]\nnoise      = [e for e in emails_data if e[\"is_noise\"]]\nactionable.sort(key=lambda e: -e[\"urgency\"])\n\n# ââ Display: Priority inbox âââââââââââââââââââââââââââââââââââââââââââââââââââ\nconsole.print()\npriority_table = Table(\n    title=f\"ð¬ Priority Inbox â {len(actionable)} actionable emails\",\n    box=box.ROUNDED, border_style=\"cyan\"\n)\npriority_table.add_column(\"Score\",   style=\"red\",    width=7,  justify=\"right\")\npriority_table.add_column(\"From\",    style=\"yellow\", width=22)\npriority_table.add_column(\"Subject\", style=\"white\",  width=40)\npriority_table.add_column(\"Age\",     style=\"dim\",    width=8)\npriority_table.add_column(\"Status\",  style=\"green\",  width=10)\n\nfor e in actionable[:20]:\n    age_str = ([f\"{int(e['age_hours'])}h\" if e[\"age_hours\"] < 48\n               else f\"{int(e['age_hours'] // 24)}d\")\n    status  = \"â replied\" if e[\"replied\"] else \"\"\n    score_colour = \"red\" if e[\"urgency\"] >= 70 else \"yellow\" if e[\"urgency\"] >= 50 else \"dim\"\n    priority_table.add_row(\n        f\"[{score_colour}]{e['urgency']}[/{score_colour}]\",\n        e[\"sender\"][:20],\n        e[\"subject\"][:38],\n        age_str,\n        status,\n    )\n\nconsole.print(priority_table)\n\n# ââ Display: Draft replies ââââââââââââââââââââââââââââââââââââââââââââââââââââ\nsig = f\"\\n\\nâ\\n{YOUR_NAME or 'Best'}{+, ' + YOUR_ROLE if YOUR_ROLE else ''}\"\ntop5 = [e for e in actionable if not e[\"replied\"]][:5]\n\nconsole.print()\nfor e in top5:\n    subj_low = e[\"subject\"].lower()\n    snip_low = e[\"snippet\"].lower()\n    greeting = f\"Hi {e['sender'].split()[0]},\"\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'll look into it right away and get back to you shortly.\"\n    elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n        body = \"Thanks for your message. To answer your question: [your answer here]\\n\\nLet me know if you need anything else.\"\n    elif \"re:\" in subj_low or \"fwd:\" 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 that's easier.\"\n    else:\n        body = \"Thanks for reaching out. I've reviewed your message and [your response here].\"\n    draft = f\"{greeting}\\n\\n{body}{sig}\"\n    console.print(Panel(\n        draft,\n        title=f\"[bold]ð Draft: Re: {e['subject'][:45]}[/bold]  [dim](urgency: {e['urgency']})[/dim]\",\n        border_style=\"yellow\"\n    ))\n\n# ââ Display: Stats ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nconsole.print()\nconsole.print(Panel(\n    f\"ð [bold]Summary[/bold]\\n\\n\"\n    f\"Scanned:    [yellow]{len(emails_data)}[/yellow] emails\\n\"\n    f\"Actionable: [cyan]{len(actionable)}[/cyan]  |  \"\n    f\"Noise: [dim]{len(noise)}[/dim]  |  \"\n    f\"Already replied: [green]{sum(1 for e in actionable if e['replied'])}[/green]\\n\"\n    f\"High priority (70+): [red]{sum(1 for e in actionable if e['urgency'] >= 70)}[/red]\",\n    border_style=\"cyan\"\n))\n\n# ââ Save report âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\ndate_str    = datetime.now().strftime(\"%Y-%m-%d\")\nreport_file = f\"inbox_report_{date_str}.md\"\nwith open(report_file, \"w\", encoding=\"utf-8\") as f:\n    f.write(f\"# ð Iris â Inbox Report â {date_str}\\n\\n\")\n    f.write(f\"**Scanned:** {len(emails_data)}  **Actionable:** {len(actionable)}  **Noise:** {len(noise)}\\n\\n\")\n    f.write(\"## Priority Emails\\n\\n| Score | From | Subject | Age |\\n|-------|------|-----------|-----|\\n\")\n    for e in actionable[:20]:\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['sender']} | {e['subject']} | {age_str} |\\n\")\n    f.write(\"\\n## Draft Replies\\n\\n\")\n    for e in top5:\n        subj_low = e[\"subject\"].lower()\n        snip_low = e[\"snippet\"].lower()\n        greeting = f\"Hi {e['sender'].split()[0]},\"\n        if any(k in subj_low or k in snip_low for k in [\"urgent\", \"asap\", \"deadline\"]):\n            body = \"Thank you for flagging this â I'll look into it right away.\"\n        elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n            body = \"Thanks for your message. To answer your question: [your answer here]\"\n        else:\n            body = \"Thanks for reaching out. [your response here]\"\n        f.write(f\"### Re: {e['subject']}\\n\\n```\\n{greeting}\\n\\n{body}{sig}\\n```\\n\\n\")\n\nconsole.print(Panel(\n    f\"[green]â Done![/green]  Report saved to [cyan]{report_file}[/cyan]\",\n    border_style=\"green\"\n))\n```\n\nFile v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn7474wrtsjp988869sw7mygz1860n2v\",\n  \"slug\": \"iris\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1778356498602\n}\n\nArchive v1.0.0: 2 files, 5784 bytes\n\nFiles: SKILL.md (14119b), _meta.json (123b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: iris\ndescription: \"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.\"\nversion: \"1.0.0\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"🌈\"\n    homepage: https://clawhub.ai/skills/iris\n    tags: [email, gmail, inbox, triage, productivity, iris, ai-drafts, time-saving]\n    envVars:\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address (e.g. you@gmail.com)\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: Gmail app password — create at myaccount.google.com/apppasswords (NOT your regular password)\n      - name: SCAN_COUNT\n        required: false\n        description: Number of recent emails to scan (default 50)\n      - name: VIP_SENDERS\n        required: false\n        description: Comma-separated VIP email addresses — always scored highest (e.g. boss@company.com,client@co.com)\n      - name: YOUR_NAME\n        required: false\n        description: Your name for reply drafts (e.g. \"Alex\")\n      - name: YOUR_ROLE\n        required: false\n        description: Your role/context for reply drafts (e.g. \"Freelance developer\" or \"Marketing manager\")\n---\n\n# 🌈 Iris — Inbox Intelligence v1.0\n\n## ⚡ Quick Start\n\n1. Go to **myaccount.google.com/apppasswords** → generate an app password for \"Mail\"\n2. Set `GMAIL_ADDRESS` and `GMAIL_APP_PASSWORD`\n3. Run — get your inbox scored, prioritised, and reply drafts ready in under 2 minutes\n\n**What you get:**\n- Every email scored 0–100 by urgency, sender importance, and content signals\n- Attachment 📎 and meeting 📅 flags — so action items are never missed\n- Fast 2-pass fetch: headers for all emails, full body only for top 5 (handles large inboxes)\n- Colour-coded priority table: 🔴 Act Now · 🟠 Today · 🟡 This Week · ⚪ Archive\n- Context-aware reply drafts for your top 5 emails based on your name/role\n- Unsubscribe candidates: emails from lists you never open\n- `inbox_report_[DATE].md` with everything organised\n\n---\n\n## 🔒 Security\n\nConnects to: `imap.gmail.com` (your credentials, read-only) · No external services\nApp passwords only scope to the specific app — revoking them in Google Account settings instantly cuts access.\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\n\n```python\nimport os, imaplib, email, re\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\nfrom rich.console import Console\nfrom rich.table import Table\nfrom rich.panel import Panel\nfrom rich import box\n\nconsole = Console()\n\nGMAIL_ADDR = os.environ.get(\"GMAIL_ADDRESS\", \"\")\nGMAIL_PASS = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\")\ntry:\n    SCAN_COUNT = int(os.environ.get(\"SCAN_COUNT\", \"50\"))\nexcept ValueError:\n    console.print(\"[yellow]⚠️  SCAN_COUNT must be a whole number — defaulting to 50[/yellow]\")\n    SCAN_COUNT = 50\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\", \"\")\nYOUR_ROLE  = os.environ.get(\"YOUR_ROLE\", \"\")\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\n        \"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are required.[/red]\\n\\n\"\n        \"Create an app password at: [bold]myaccount.google.com/apppasswords[/bold]\\n\"\n        \"Choose: Mail → Your computer\",\n        title=\"Setup Required\", border_style=\"red\"))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\"urgent\",\"asap\",\"deadline\",\"immediately\",\"action required\",\"time sensitive\",\n                   \"overdue\",\"past due\",\"invoice\",\"payment\",\"legal\",\"lawsuit\",\"critical\",\"emergency\"]\nREPLY_KEYWORDS  = [\"?\",\"question\",\"can you\",\"could you\",\"please\",\"request\",\"following up\",\"reminder\"]\nNOISE_PATTERNS  = [r\"unsubscribe\",r\"newsletter\",r\"no-reply\",r\"noreply\",r\"marketing@\",\n                   r\"notifications?@\",r\"donotreply\",r\"info@.*\\.(com|net)\"]\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            try: result += part.decode(enc or \"utf-8\", errors=\"replace\")\n            except Exception: result += part.decode(\"latin-1\", errors=\"replace\")\n        else:\n            result += str(part)\n    return result.strip()\n\nMEETING_KEYWORDS = [\"calendar invite\",\"meeting request\",\"zoom\",\"teams meeting\",\"google meet\",\n                    \"webex\",\"invited you\",\"join the meeting\",\"scheduled a\",\"let's meet\",\"video call\"]\n\ndef score_email(subject, sender, body_snippet, age_hours, has_attachment=False):\n    score = 50\n    subj_lower   = subject.lower()\n    body_lower   = body_snippet.lower()\n    sender_lower = sender.lower()\n\n    # Noise penalty applied first — VIPs can still overcome it\n    is_noise = any(re.search(p, sender_lower) for p in NOISE_PATTERNS)\n    if is_noise:\n        score -= 40\n\n    # VIP sender\n    if any(vip in sender_lower for vip in VIP_LIST):\n        score += 40\n\n    # Urgency keywords in subject\n    for kw in URGENT_KEYWORDS:\n        if kw in subj_lower:\n            score += 15\n            break\n\n    # Reply signals\n    if any(kw in subj_lower or kw in body_lower for kw in REPLY_KEYWORDS):\n        score += 10\n\n    # Meeting / calendar invite\n    if any(kw in subj_lower or kw in body_lower for kw in MEETING_KEYWORDS):\n        score += 20\n\n    # Has attachment — likely needs action\n    if has_attachment:\n        score += 15\n\n    # Freshness\n    if age_hours < 2:   score += 20\n    elif age_hours < 8: score += 10\n    elif age_hours > 48: score -= 10\n\n    # RE: or FWD: (thread, likely needs response)\n    if re.match(r\"^(re:|fwd?:)\", subj_lower):\n        score += 5\n\n    return max(0, min(100, score))\n\ndef get_priority(score):\n    if score >= 80: return (\"🔴\", \"Act Now\",   \"red\")\n    if score >= 60: return (\"🟠\", \"Today\",     \"orange3\")\n    if score >= 40: return (\"🟡\", \"This Week\", \"yellow\")\n    return             (\"⚪\", \"Archive\",    \"dim\")\n\nconsole.print(Panel(\"[bold cyan]📧 🌈 Iris — Inbox Intelligence v1.0[/bold cyan]\\nConnecting to Gmail...\", border_style=\"cyan\"))\n\ntry:\n    mail = imaplib.IMAP4_SSL(\"imap.gmail.com\")\n    mail.login(GMAIL_ADDR, GMAIL_PASS)\n    mail.select(\"inbox\")\nexcept Exception as e:\n    console.print(Panel(f\"[red]IMAP login failed: {e}[/red]\\nCheck GMAIL_ADDRESS and GMAIL_APP_PASSWORD.\", title=\"Login Error\", border_style=\"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 []\nif not all_ids:\n    console.print(Panel(\"[yellow]Inbox is empty — nothing to triage.[/yellow]\", border_style=\"yellow\"))\n    raise SystemExit(0)\nrecent_ids = all_ids[-SCAN_COUNT:] if len(all_ids) > SCAN_COUNT else all_ids\nrecent_ids = list(reversed(recent_ids))\n\nconsole.print(f\"[dim]Scanning {len(recent_ids)} emails...[/dim]\")\n\n# Pass 1: fetch headers only (fast) — score every email\nemails_data = []\nfor eid in recent_ids:\n    try:\n        _, data = mail.fetch(eid, \"(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])\")\n        raw_header = data[0][1]\n        msg = email.message_from_bytes(raw_header)\n\n        subject  = decode_str(msg.get(\"Subject\", \"(no subject)\"))\n        sender   = decode_str(msg.get(\"From\", \"\"))\n        date_hdr = msg.get(\"Date\", \"\")\n        has_attachment = False  # unknown at header stage; updated in pass 2 for top5\n\n        try:\n            dt    = parsedate_to_datetime(date_hdr)\n            age_h = (datetime.now(timezone.utc) - dt.astimezone(timezone.utc)).total_seconds() / 3600\n        except Exception:\n            age_h = 24\n\n        score = score_email(subject, sender, \"\", age_h, has_attachment)\n        icon, priority, color = get_priority(score)\n        emails_data.append({\n            \"eid\": eid, \"subject\": subject[:80], \"sender\": sender[:50],\n            \"score\": score, \"priority\": priority, \"icon\": icon, \"color\": color,\n            \"age_hours\": round(age_h, 1), \"snippet\": \"\", \"is_noise\": score < 20,\n        })\n    except Exception:\n        continue\n\nemails_data.sort(key=lambda x: x[\"score\"], reverse=True)\n\n# Pass 2: fetch full RFC822 only for top 5 non-noise — get body + attachments for drafts\ntop5_ids = [e[\"eid\"] for e in emails_data if not e[\"is_noise\"]][:5]\neid_to_idx = {e[\"eid\"]: i for i, e in enumerate(emails_data)}\nfor eid in top5_ids:\n    try:\n        _, data = mail.fetch(eid, \"(RFC822)\")\n        raw = data[0][1]\n        msg = email.message_from_bytes(raw)\n        body_snippet = \"\"\n        has_attachment = False\n        for part in msg.walk():\n            ct = part.get_content_type()\n            cd = str(part.get(\"Content-Disposition\", \"\"))\n            if \"attachment\" in cd:\n                has_attachment = True\n            if ct == \"text/plain\" and \"attachment\" not in cd and not body_snippet:\n                try:\n                    body_snippet = part.get_payload(decode=True).decode(\"utf-8\", errors=\"replace\")[:300]\n                except Exception:\n                    pass\n        idx = eid_to_idx.get(eid)\n        if idx is not None:\n            e = emails_data[idx]\n            e[\"snippet\"] = body_snippet[:150]\n            e[\"has_attachment\"] = has_attachment\n            # Re-score with body + attachment info\n            new_score = score_email(e[\"subject\"], e[\"sender\"], body_snippet, e[\"age_hours\"], has_attachment)\n            e[\"score\"] = new_score\n            e[\"icon\"], e[\"priority\"], e[\"color\"] = get_priority(new_score)\n            e[\"is_noise\"] = new_score < 20\n    except Exception:\n        continue\n\nmail.logout()\nemails_data.sort(key=lambda x: x[\"score\"], reverse=True)\n\n# Rich table\ntable = Table(title=f\"📧 🌈 Iris — Inbox Intelligence — {len(emails_data)} emails scanned\", box=box.ROUNDED, border_style=\"cyan\")\ntable.add_column(\"Priority\", width=12)\ntable.add_column(\"Score\", width=6, justify=\"center\")\ntable.add_column(\"Subject\", width=45)\ntable.add_column(\"From\", width=28)\ntable.add_column(\"Age\", width=6, justify=\"center\")\ntable.add_column(\"Flags\", width=6)\nfor e in emails_data[:20]:\n    flags = \"\"\n    if e.get(\"has_attachment\"): flags += \"📎\"\n    if any(k in e[\"subject\"].lower() or k in e[\"snippet\"].lower() for k in [\"zoom\",\"meet\",\"calendar invite\",\"meeting\"]): flags += \"📅\"\n    table.add_row(\n        f\"[{e['color']}]{e['icon']} {e['priority']}[/{e['color']}]\",\n        str(e[\"score\"]), e[\"subject\"], e[\"sender\"], f\"{e['age_hours']:.0f}h\", flags)\nconsole.print(table)\n\n# Draft replies for top 5 — contextual openers based on subject signals\ntop5 = [e for e in emails_data if not e[\"is_noise\"]][:5]\ndrafts = []\nURGENT_KW   = [\"urgent\",\"asap\",\"deadline\",\"overdue\",\"invoice\",\"payment\",\"action required\"]\nQUESTION_KW = [\"?\",\"can you\",\"could you\",\"please advise\",\"let me know\",\"following up\",\"reminder\"]\nfor e in top5:\n    greeting = \"Hi there,\"  # YOUR_NAME is for the signature, not the greeting\n    role_line = f\"\\n{YOUR_ROLE}\" if YOUR_ROLE else \"\"\n    sig      = f\"\\n\\nBest,\\n{YOUR_NAME}{role_line}\" if YOUR_NAME else \"\"\n    subj_low = e[\"subject\"].lower()\n    snip_low = e[\"snippet\"].lower()\n    if any(k in subj_low for k in URGENT_KW):\n        body = \"Thank you for flagging this — I'll look into it right away and get back to you shortly.\"\n    elif any(k in subj_low or k in snip_low for k in QUESTION_KW):\n        body = \"Thanks for your message. To answer your question: [your answer here]\\n\\nLet me know if you need anything else.\"\n    elif \"re:\" in subj_low or \"fwd:\" 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 that's easier.\"\n    else:\n        body = \"Thanks for reaching out. I've reviewed your message and [your response here].\"\n    draft = f\"{greeting}\\n\\n{body}{sig}\"\n    drafts.append({\"subject\": e[\"subject\"], \"draft\": draft})\n\nnoise = [e for e in emails_data if e[\"is_noise\"]]\n\n# Save report\ndate_str = datetime.now().strftime(\"%Y-%m-%d\")\nreport_file = f\"inbox_report_{date_str}.md\"\nwith open(report_file, \"w\", encoding=\"utf-8\") as f:\n    f.write(f\"# 🌈 Iris — Inbox Intelligence — {date_str}\\n\\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\")\n    f.write(\"## 🔴 Action Required\\n\\n\")\n    for e in [x for x in emails_data if x[\"priority\"] == \"Act Now\"][:10]:\n        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\")\n    f.write(\"## ✉️ Reply Drafts\\n\\n\")\n    if drafts:\n        for d in drafts:\n            f.write(f\"### Re: {d['subject']}\\n\\n{d['draft']}\\n\\n---\\n\\n\")\n    else:\n        f.write(\"_No reply drafts generated — top emails were all newsletters or noise. Check Unsubscribe Candidates below._\\n\\n\")\n    f.write(\"## 🗑️ Unsubscribe Candidates\\n\\n\")\n    for e in noise[:15]:\n        f.write(f\"- {e['sender']} — {e['subject']}\\n\")\n\nact_now = len([e for e in emails_data if e[\"priority\"] == \"Act Now\"])\nconsole.print(Panel(\n    f\"[bold green]✅ Inbox triaged![/bold green]\\n\\n\"\n    f\"[red bold]🔴 {act_now} Act Now[/red bold]\\n\"\n    f\"[orange3]{len([e for e in emails_data if e['priority']=='Today'])} Today[/orange3]\\n\"\n    f\"[yellow]{len([e for e in emails_data if e['priority']=='This Week'])} This Week[/yellow]\\n\"\n    f\"[dim]{len(noise)} Unsubscribe candidates[/dim]\\n\\n\"\n    f\"📝 Full report: [cyan]{report_file}[/cyan]\\n\\n\"\n    f\"[bold]What to do next:[/bold]\\n\"\n    f\"  1. Handle all 🔴 Act Now emails first\\n\"\n    f\"  2. Use the reply drafts as starting points\\n\"\n    f\"  3. Unsubscribe from the noise senders\\n\"\n    f\"  4. Run daily for a permanently clean inbox\",\n    title=\"📧 Triage Complete\", border_style=\"green\"))\n```\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn7474wrtsjp988869sw7mygz1860n2v\",\n  \"slug\": \"iris\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1777962399481\n}","readmeExcerpt":"Skill: Iris — Rainbow Messenger Owner: occupythemilkyway Summary: 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 act... Tags: email:1.0.1, gmail:1.0.1, inbox:1.0.1, latest:1.0.4, productivity:1.0.1, triage:1.0.1 Version history: v1.0.4 | 2026-05-24T18:48:44.956Z | user Version 1.0.4 - Fixed text encoding issues ","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"pip3 install rich --break-system-packages --quiet"},{"language":"bash","snippet":"openclaw skills install iris-pro\n# Set LICENSE_KEY env var to your key from Ko-fi, then run"},{"language":"python","snippet":"import os, imaplib, email, re\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\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\nGMAIL_ADDR = os.environ.get(\"GMAIL_ADDRESS\", \"\").strip()\nGMAIL_PASS = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\").strip()\ntry:\n    SCAN_COUNT = int(os.environ.get(\"SCAN_COUNT\", \"50\"))\nexcept ValueError:\n    console.print(\"[yellow]⚠️  SCAN_COUNT must be a whole number — defaulting to 50[/yellow]\")\n    SCAN_COUNT = 50\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()\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\n        \"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are both required.[/red]\\n\\n\"\n        \"How to create an app password:\\n\"\n        \"1. Go to [bold]myaccount.google.com/apppasswords[/bold]\\n\"\n        \"2. Select Mail → Other (Custom name)\\n\"\n        \"3. Copy the 16-character password\\n\"\n        \"4. Set GMAIL_APP_PASSWORD to that value\",\n        title=\"[bold red]⚙️  Setup Required[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\n    \"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\",\n]\nREPLY_KEYWORDS = [\n    \"?\", \"question\", \"can you\", \"could you\", \"please\", \"request\",\n    \"following up\", \"follow-up\", \"reminder\", \"let me know\", \"thoughts\",\n]\nQUESTION_KW = [\"?\", \"question\", \"can you\", \"could you\", \"help\", \"assist\"]\nNOISE_PATTERNS = [\n    r\"unsubscribe\", r\"newsletter\", r\"no-reply@\", r\"noreply@\",\n    r\"marketing@\", r\"notifications?@\", r\"donotreply@\""},{"language":"bash","snippet":"pip3 install rich --break-system-packages --quiet"},{"language":"python","snippet":"import os, imaplib, email, re\nfrom email.header import decode_header\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\nfrom rich.console import Console\nfrom rich.table import Table\nfrom rich.panel import Panel\nfrom rich import box\n\nconsole = Console()\n\nGMAIL_ADDR = os.environ.get(\"GMAIL_ADDRESS\", \"\").strip()\nGMAIL_PASS = os.environ.get(\"GMAIL_APP_PASSWORD\", \"\").strip()\ntry:\n    SCAN_COUNT = int(os.environ.get(\"SCAN_COUNT\", \"50\"))\nexcept ValueError:\n    console.print(\"[yellow]â ï¸  SCAN_COUNT must be a whole number â defaulting to 50[/yellow]\")\n    SCAN_COUNT = 50\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()\n\nif not GMAIL_ADDR or not GMAIL_PASS:\n    console.print(Panel(\n        \"[red]GMAIL_ADDRESS and GMAIL_APP_PASSWORD are both required.[/red]\\n\\n\"\n        \"How to create an app password:\\n\"\n        \"1. Go to [bold]myaccount.google.com/apppasswords[/bold]\\n\"\n        \"2. Select Mail â Other (Custom name)\\n\"\n        \"3. Copy the 16-character password\\n\"\n        \"4. Set GMAIL_APP_PASSWORD to that value\",\n        title=\"[bold red]âï¸  Setup Required[/bold red]\",\n        border_style=\"red\"\n    ))\n    raise SystemExit(1)\n\nURGENT_KEYWORDS = [\n    \"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\",\n]\nREPLY_KEYWORDS = [\n    \"?\", \"question\", \"can you\", \"could you\", \"please\", \"request\",\n    \"following up\", \"follow-up\", \"reminder\", \"let me know\", \"thoughts\",\n]\nQUESTION_KW = [\"?\", \"question\", \"can you\", \"could you\", \"help\", \"assist\"]\nNOISE_PATTERNS = [\n    r\"unsubscribe\", r\"newsletter\", r\"no-reply@\", r\"noreply@\",\n    r\"marketing@\", r\"notifications?@\", r\"donotreply@\",\n    r\"@"},{"language":"text","snippet":"File v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn7474wrtsjp988869sw7mygz1860n2v\",\n  \"slug\": \"iris\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1778356498602\n}\n\nArchive v1.0.0: 2 files, 5784 bytes\n\nFiles: SKILL.md (14119b), _meta.json (123b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: iris\ndescription: \"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.\"\nversion: \"1.0.0\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"🌈\"\n    homepage: https://clawhub.ai/skills/iris\n    tags: [email, gmail, inbox, triage, productivity, iris, ai-drafts, time-saving]\n    envVars:\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address (e.g. you@gmail.com)\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: Gmail app password — create at myaccount.google.com/apppasswords (NOT your regular password)\n      - name: SCAN_COUNT\n        required: false\n        description: Number of recent emails to scan (default 50)\n      - name: VIP_SENDERS\n        required: false\n        description: Comma-separated VIP email addresses — always scored highest (e.g. boss@company.com,client@co.com)\n      - name: YOUR_NAME\n        required: false\n        description: Your name for reply drafts (e.g. \"Alex\")\n      - name: YOUR_ROLE\n        required: false\n        description: Your role/context for reply drafts (e.g. \"Freelance developer\" or \"Marketing manager\")\n---\n\n# 🌈 Iris — Inbox Intelligence v1.0\n\n## ⚡ Quick Start\n\n1. Go to **myaccount.google.com/apppasswords** → generate an app password for \"Mail\"\n2. Set `GMAIL_ADDRESS` and `GMAIL_APP_PASSWORD`\n3. Run — get your inbox scored, prioritised, and reply drafts ready in under 2 minutes\n\n**What you get:**\n- E"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: iris\ndescription: \"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.\"\nversion: \"1.0.4\"\nmetadata:\n  openclaw:\n    requires:\n      env: [GMAIL_ADDRESS, GMAIL_APP_PASSWORD]\n      bins: [python3, pip3]\n    primaryEnv: GMAIL_ADDRESS\n    emoji: \"🌈📬\"\n    homepage: https://clawhub.ai/occupythemilkyway/iris\n    tags: [email, gmail, inbox, triage, productivity, iris, ai-drafts, time-saving]\n    envVars:\n      - name: GMAIL_ADDRESS\n        required: true\n        description: Your Gmail address (e.g. you@gmail.com)\n      - name: GMAIL_APP_PASSWORD\n        required: true\n        description: \"Gmail app password — create at myaccount.google.com/apppasswords (NOT your regular password). Choose: Mail → Other\"\n      - name: SCAN_COUNT\n        required: false\n        description: \"Number of recent emails to scan (default: 50)\"\n        default: \"50\"\n      - name: VIP_SENDERS\n        required: false\n        description: \"Comma-separated list of important sender email addresses or domains that should always score high\"\n        default: \"\"\n      - name: YOUR_NAME\n        required: false\n        description: \"Your name for personalising draft replies (e.g. Alex)\"\n        default: \"\"\n      - name: YOUR_ROLE\n        required: false\n        description: \"Your role for context-aware replies (e.g. Product Manager)\"\n        default: \"\"\n---\n\n# Iris — Inbox Intelligence\n\nRead your last 50 emails, score every one by urgency, get draft replies for your top 5, and see a clean action list — all in under 2 minutes.\n\n## What you get\n\n- **Urgency scoring** (0-100) based on keywords, sender importance, age, and reply signals\n- **Priority inbox**: top 5 emails that need action today, with one-click draft replies\n- **Noise filter**: newsletters, no-reply addresses, and marketing email suppressed automatically\n- **VIP tracking**: set specific senders or domains that always score high\n- **Daily report saved** to markdown — reference it any time during the day\n\n## Setup (one-time, 2 minutes)\n\n1. Go to **myaccount.google.com/apppasswords**\n2. Create an app password — select **Mail** → **Other (Custom name)**\n3. Copy the 16-character password\n4. Set `GMAIL_APP_PASSWORD` to that password (not your Google login password)\n\n## 🔒 Security\n\nConnects to: `imap.gmail.com` (your credentials, read-only). No external services contacted.\nApp passwords only scope to the specific app — revoke them instantly in Google Account settings.\n\n---\n\n## Step 1 — Install\n\n```bash\npip3 install rich --break-system-packages --quiet\n```\n\n---\n\n\n---\n\n## ⚡ Upgrade to Iris Pro\n\n👉 **Get Iris Pro** → **ko-fi.com/s/f75940a0ce** — $9 one-time\n\n```bash\nopenclaw skills install iris-pro\n# Set LICENSE_KEY env var to your key from Ko-fi, then run\n```\n\n💰 **Bundle deal:** all 5 Pro skills for **$29** → **ko"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7474wrtsjp988869sw7mygz1860n2v\",\n  \"slug\": \"iris\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1779648524956\n}"},{"path":"skill-card.md","content":"## Description:\n\nIris reads a Gmail inbox, scores recent email by urgency and sender importance, drafts replies for the top five actionable messages, and writes a daily action report.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[occupythemilkyway](https://clawhub.ai/user/occupythemilkyway)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nIndividuals and teams use Iris to triage recent Gmail messages, identify priority items, prepare draft replies, and keep a local markdown report for daily follow-up.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill handles Gmail credentials and email-derived content.\n\nMitigation: Use a Gmail app password, keep credentials in environment variables, and revoke the app password from Google Account settings when it is no longer needed.\n\nRisk: The skill writes sender, subject, urgency, and draft-reply data to a local markdown report.\n\nMitigation: Run it only in a trusted working directory and avoid regulated or highly sensitive mail unless report writing is changed or disabled.\n\nRisk: The documented install command uses system Python package installation.\n\nMitigation: Install and run the skill in a dedicated virtual environment.\n\n## Reference(s):\n\n- [Iris ClawHub skill page](https://clawhub.ai/occupythemilkyway/skills/iris)\n- [Iris ClawHub homepage](https://clawhub.ai/occupythemilkyway/iris)\n- [OccupyTheMilkyWay publisher profile](https://clawhub.ai/user/occupythemilkyway)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Console output and markdown report with draft-reply text and setup commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Writes a local inbox_report_<date>.md file containing email-derived sender, subject, priority, and draft-reply content.]\n\n## Skill Version(s):\n\n1.0.4 (source: frontmatter and server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"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 act... Skill: Iris — Rainbow Messenger Owner: occupythemilkyway Summary: 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 act... Tags: email:1.0.1, gmail:1.0.1, inbox:1.0.1, latest:1.0.4, productivity:1.0.1, triage:1.0.1 Version history: v1.0.4 | 2026-05-24T18:48:44.956Z | user Version 1.0.4 - Fixed text encoding issues","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1169,"uniquenessScore":49,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-12T01:50:17.575Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-12T16:56:47.166Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}