Iris——彩虹信使。读取Gmail收件箱,根据紧急程度和发件人重要性对每封邮件评分,为排名前5的邮件撰写回复,并生成每日行动报告。
Iris — Inbox Intelligence. 读完你最后的50封邮件,每封紧急得分,获得你前5名的回复草稿,并看到干净的动作列表,每两分钟内就全部收到是一项面向实际任务的技能,主要用于紧急的Scor。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。
实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。
执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。
读取你最近的 50 封邮件,为每封邮件按紧急程度打分(0–100),自动生成前 5 封高优先级邮件的回复草稿,并呈现一份清晰待办清单——全程耗时不到 2 分钟。
GMAIL_APP_PASSWORD仅连接至:imap.gmail.com(仅使用你的凭证,且为只读权限)。不调用任何外部服务。
应用专用密码作用域严格限定于本应用,可在 Google 账户设置中随时撤销。
pip3 install rich --break-system-packages --quiet
👉 获取 Iris Pro → ko-fi.com/s/f75940a0ce —— 一次性付费 $9
openclaw skills install iris-pro # 将你在 Ko-fi 获取的 LICENSE_KEY 设为环境变量,然后运行
💰 套餐优惠:全部 5 项 Pro 技能仅需 $29 → ko-fi.com/s/7625accf3f(立省 $16)
import os, imaplib, email, re
from email.header import decode_header
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box
FENCE = chr(96) * 3
console = Console()
GMAIL_ADDR = os.environ.get("GMAIL_ADDRESS", "").strip()
GMAIL_PASS = os.environ.get("GMAIL_APP_PASSWORD", "").strip()
try:
SCAN_COUNT = int(os.environ.get("SCAN_COUNT", "50"))
except ValueError:
console.print("[yellow]⚠️ SCAN_COUNT 必须为整数 —— 默认使用 50[/yellow]")
SCAN_COUNT = 50
VIP_RAW = os.environ.get("VIP_SENDERS", "")
VIP_LIST = [v.strip().lower() for v in VIP_RAW.split(",") if v.strip()]
YOUR_NAME = os.environ.get("YOUR_NAME", "").strip()
YOUR_ROLE = os.environ.get("YOUR_ROLE", "").strip()
if not GMAIL_ADDR or not GMAIL_PASS:
console.print(Panel(
"[red]GMAIL_ADDRESS 和 GMAIL_APP_PASSWORD 均为必需项。[/red]nn"
"如何创建应用专用密码:n"
"1. 访问 [bold]myaccount.google.com/apppasswords[/bold]n"
"2. 选择 Mail → Other (Custom name)n"
"3. 复制生成的 16 位密码n"
"4. 将 GMAIL_APP_PASSWORD 设为该值",
title="[bold red]⚙️ 需要完成配置[/bold red]",
border_style="red"
))
raise SystemExit(1)
URGENT_KEYWORDS = [
"urgent", "asap", "deadline", "immediately", "action required", "time sensitive",
"overdue", "past due", "invoice", "payment due", "legal", "lawsuit", "critical",
"emergency", "final notice", "expires", "expiring", "last chance",
]
REPLY_KEYWORDS = [
"?", "question", "can you", "could you", "please", "request",
"following up", "follow-up", "reminder", "let me know", "thoughts",
]
QUESTION_KW = ["?", "question", "can you", "could you", "help", "assist"]
NOISE_PATTERNS = [
r"unsubscribe", r"newsletter", r"no-reply@", r"noreply@",
r"marketing@", r"notifications?@", r"donotreply@",
r"@.*.(mailchim|sendgrid|constantcontact|klaviyo)",
]
NOISE_SUBJECTS = [
"sale", "% off", "deal", "offer", "promo", "subscribe", "newsletter",
"weekly digest", "monthly update", "announcement",
]
def decode_str(s):
if not s:
return ""
parts = decode_header(s)
result = []
for part, enc in parts:
if isinstance(part, bytes):
try:
result.append(part.decode(enc or "utf-8", errors="replace"))
except Exception:
result.append(part.decode("utf-8", errors="replace"))
else:
result.append(str(part))
return " ".join(result)
def is_noise(sender: str, subject: str) -> bool:
text = (sender + " " + subject).lower()
for pat in NOISE_PATTERNS:
if re.search(pat, text):
return True
for kw in NOISE_SUBJECTS:
if kw in subject.lower():
return True
return False
def score_email(subject: str, snippet: str, sender: str, age_hours: float, has_replied: bool) -> int:
score = 50
subj_low = subject.lower()
snip_low = snippet.lower()
# 紧急度关键词
for kw in URGENT_KEYWORDS:
if kw in subj_low or kw in snip_low:
score += 20
break
# 是否需回复
for kw in REPLY_KEYWORDS:
if kw in subj_low or kw in snip_low:
score += 10
break
# VIP 发件人
sender_low = sender.lower()
for vip in VIP_LIST:
if vip in sender_low:
score += 25
break
# 时效性加减分
if age_hours < 2:
score += 5
elif age_hours < 24:
score += 0
elif age_hours > 48:
score -= 10
elif age_hours > 120:
score -= 20
# 已回复则降分
if has_replied:
score -= 15
return max(0, min(score, 100))
# ── 连接 Gmail ──────────────────────────────────────────────────────────
console.print(Panel.fit(
f"[bold cyan]🌈 Iris — Inbox Intelligence[/bold cyan]n"
f"正在扫描 [yellow]{SCAN_COUNT}[/yellow] 封来自 [green]{GMAIL_ADDR}[/green] 的最新邮件",
border_style="cyan"
))
try:
mail = imaplib.IMAP4_SSL("imap.gmail.com", 993)
mail.login(GMAIL_ADDR, GMAIL_PASS)
mail.select("INBOX", readonly=True)
except imaplib.IMAP4.error as e:
console.print(Panel(
f"[red]IMAP 登录失败:[/red] {e}nn"
"检查清单:n"
"• GMAIL_ADDRESS 是否正确?(需填写完整邮箱地址,而非仅用户名)n"
"• GMAIL_APP_PASSWORD 是否为 16 位应用专用密码(非你的 Google 登录密码)?n"
"• 是否已在 Gmail 设置 → 查看所有设置 → 转发和 POP/IMAP 中启用 IMAP?",
title="[bold red]登录错误[/bold red]",
border_style="red"
))
raise SystemExit(1)
except Exception as e:
console.print(f"[red]连接异常:{e}[/red]")
raise SystemExit(1)
_, msg_ids = mail.search(None, "ALL")
all_ids = msg_ids[0].split() if msg_ids and msg_ids[0] else []
if not all_ids:
console.print(Panel("[yellow]收件箱为空 —— 无可处理邮件。[/yellow]", border_style="yellow"))
raise SystemExit(0)
recent_ids = all_ids[-SCAN_COUNT:] if len(all_ids) > SCAN_COUNT else all_ids
recent_ids = list(reversed(recent_ids)) # 最新邮件在前
# ── 获取并评分邮件 ────────────────────────────────────────────────────
emails_data = []
now = datetime.now(timezone.utc)
console.print(f"[dim]正在获取 {len(recent_ids)} 封邮件…[/dim]")
for uid in recent_ids:
try:
_, raw = mail.fetch(uid, "(RFC822.HEADER FLAGS)")
if not raw or not raw[0]:
continue
raw_header = raw[0][1] if isinstance(raw[0], tuple) else raw[0]
msg = email.message_from_bytes(raw_header)
subject = decode_str(msg.get("Subject", "(no subject)"))
sender = decode_str(msg.get("From", ""))
date_str = msg.get("Date", "")
flags_raw = raw[0][0] if isinstance(raw[0], tuple) else b""
has_replied = b"\Answered" in flags_raw
# 解析发送时间
try:
sent_dt = parsedate_to_datetime(date_str)
if sent_dt.tzinfo is None:
sent_dt = sent_dt.replace(tzinfo=timezone.utc)
age_hours = (now - sent_dt).total_seconds() / 3600
except Exception:
age_hours = 0
# 获取正文片段
body_snippet = ""
try:
_, raw_body = mail.fetch(uid, "(BODY[TEXT]<0.300>)")
if raw_body and raw_body[0] and isinstance(raw_body[0], tuple):
raw_b = raw_body[0][1]
if raw_b:
body_snippet = raw_b.decode("utf-8", errors="replace").strip()[:200]
except Exception:
pass
noise = is_noise(sender, subject)
urgency = score_email(subject, body_snippet, sender, age_hours, has_replied)
# 提取发件人姓名与邮箱
sender_match = re.search(r'"?([^"<]+)"?s*<([^>]+)>', sender)
if sender_match:
sender_name = sender_match.group(1).strip()
sender_email = sender_match.group(2).strip()
else:
sender_name = sender
sender_email = sender
emails_data.append({
"uid": uid,
"subject": subject,
"sender": sender_name,
"sender_email": sender_email,
"age_hours": age_hours,
"snippet": body_snippet,
"urgency": urgency,
"is_noise": noise,
"replied": has_replied,
})
except Exception:
continue
mail.logout()
# ── 排序与筛选 ───────────────────────────────────────────────────────────
actionable = [e for e in emails_data if not e["is_noise"]]
noise = [e for e in emails_data if e["is_noise"]]
actionable.sort(key=lambda e: -e["urgency"])
# ── 展示:优先收件箱 ───────────────────────────────────────────────────
console.print()
priority_table = Table(
title=f"📬 优先收件箱 —— {len(actionable)} 封待处理邮件",
box=box.ROUNDED, border_style="cyan"
)
priority_table.add_column("评分", style="red", width=7, justify="right")
priority_table.add_column("发件人", style="yellow", width=22)
priority_table.add_column("主题", style="white", width=40)
priority_table.add_column("时效", style="dim", width=8)
priority_table.add_column("状态", style="green", width=10)
for e in actionable[:20]:
age_str = (f"{int(e['age_hours'])}h" if e["age_hours"] < 48
else f"{int(e['age_hours'] // 24)}d")
status = "✅ 已回复" if e["replied"] else ""
score_colour = "red" if e["urgency"] >= 70 else "yellow" if e["urgency"] >= 50 else "dim"
priority_table.add_row(
f"[{score_colour}]{e['urgency']}[/{score_colour}]",
e["sender"][:20],
e["subject"][:38],
age_str,
status,
)
console.print(priority_table)
# ── 展示:回复草稿 ────────────────────────────────────────────────────
sig = f"nn—n{YOUR_NAME or 'Best'}{', ' + YOUR_ROLE if YOUR_ROLE else ''}"
top5 = [e for e in actionable if not e["replied"]][:5]
console.print()
for e in top5:
subj_low = e["subject"].lower()
snip_low = e["snippet"].lower()
greeting = f"Hi {e['sender'].split()[0]},"
if any(k in subj_low or k in snip_low for k in ["urgent", "asap", "deadline", "overdue"]):
body = "Thank you for flagging this — I'll look into it right away and get back to you shortly."
elif any(k in subj_low or k in snip_low for k in QUESTION_KW):
body = "Thanks for your message. To answer your question: [your answer here]nnLet me know if you need anything else."
elif "re:" in subj_low or "fwd:" in subj_low:
body = "Thanks for the follow-up. Here's where things stand: [brief update]nnHappy to jump on a call if that's easier."
else:
body = "Thanks for reaching out. I've reviewed your message and [your response here]."
draft = f"{greeting}nn{body}{sig}"
console.print(Panel(
draft,
title=f"[bold]📝 草稿:Re: {e['subject'][:45]}[/bold] [dim](紧急度:{e['urgency']})[/dim]",
border_style="yellow"
))
# ── 展示:统计信息 ────────────────────────────────────────────────────────────
console.print()
console.print(Panel(
f"📊 [bold]汇总[/bold]nn"
f"已扫描: [yellow]{len(emails_data)}[/yellow] 封邮件n"
f"待处理: [cyan]{len(actionable)}[/cyan] | "
f"噪音邮件: [dim]{len(noise)}[/dim] | "
f"已回复: [green]{sum(1 for e in actionable if e['replied'])}[/green]n"
f"高优先级(70+): [red]{sum(1 for e in actionable if e['urgency'] >= 70)}[/red]",
border_style="cyan"
))
# ── 保存报告 ───────────────────────────────────────────────────────────────
date_str = datetime.now().strftime("%Y-%m-%d")
report_file = f"inbox_report_{date_str}.md"
with open(report_file, "w", encoding="utf-8") as f:
f.write(f"# 🌈 Iris — 收件箱报告 — {date_str}nn")
f.write(f"**已扫描:** {len(emails_data)} **待处理:** {len(actionable)} **噪音邮件:** {len(noise)}nn")
f.write("## 优先邮件nn| 评分 | 发件人 | 主题 | 时效 |n|------|------|------|------|n")
for e in actionable[:20]:
age_str = f"{int(e['age_hours'])}h" if e["age_hours"] < 48 else f"{int(e['age_hours']//24)}d"
f.write(f"| {e['urgency']} | {e['sender']} | {e['subject']} | {age_str} |n")
f.write("n## 回复草稿nn")
for e in top5:
subj_low = e["subject"].lower()
snip_low = e["snippet"].lower()
greeting = f"Hi {e['sender'].split()[0]},"
if any(k in subj_low or k in snip_low for k in ["urgent", "asap", "deadline"]):
body = "Thank you for flagging this — I'll look into it right away."
elif any(k in subj_low or k in snip_low for k in QUESTION_KW):
body = "Thanks for your message. To answer your question: [your answer here]"
else:
body = "Thanks for reaching out. [your response here]"
f.write(f"### Re: {e['subject']}nn{FENCE}n{greeting}nn{body}{sig}n{FENCE}nn")
console.print(Panel(
f"[green]✅ 完成![/green] 报告已保存至 [cyan]{report_file}[/cyan]",
border_style="green"
))
相关专题
热门下载
相关下载
精品课程
共6课时 | 54.7万人学习
共89课时 | 134.1万人学习
共49课时 | 82.6万人学习
最新文章