diff --git a/src/api/mail_routes.py b/src/api/mail_routes.py index 17eb216..f0bad7c 100644 --- a/src/api/mail_routes.py +++ b/src/api/mail_routes.py @@ -182,22 +182,83 @@ async def get_mail(mail_id: str): @router.post("") async def send_mail(body: Dict[str, Any]): - """发送 Mail(创建 Task)""" + """发送 Mail(创建 Task) + + API 层防御(A1-A10): + A1: from 必填 + A9: from 必须是有效 Agent + A5: in_reply_to 存在性校验 + A6/A7: 自动纠正 to + A2: to 必填(非回复) + A3: from != to 防自环 + A4: to 必须是有效 Agent + A8: 回复权限校验(严格 1 对 1) + A10: 正文非空 + """ bb = _bb() + valid_agents = _get_valid_agents() + auto_corrected = None + + # --- A1: from 必填 --- + from_agent = body.get("from", "").strip() + if not from_agent: + raise HTTPException(400, "`from` 必填") + + # --- A9: from 必须是有效 Agent --- + if from_agent not in valid_agents: + raise HTTPException(400, f"`from` 不是有效的 Agent: {from_agent}") + + # --- A5/A6/A7/A8: in_reply_to 处理 --- + in_reply_to = body.get("in_reply_to") + original = None + if in_reply_to: + original = bb.get_task(in_reply_to) + # A5: 原邮件必须存在 + if not original: + raise HTTPException(400, f"回复的邮件不存在: {in_reply_to}") + + orig_from = original.assigned_by or "" + orig_to = original.assignee or "" + + # A8: 只有原邮件的双方能回复(严格 1 对 1) + if from_agent not in (orig_from, orig_to): + raise HTTPException(400, f"只有邮件的发送者或接收者可以回复") + + # A6/A7: 自动纠正 to → 原邮件发件者 + to_agent = body.get("to", "").strip() + corrected_to = orig_from # 回复方向固定: reply → original sender + if to_agent and to_agent != corrected_to: + auto_corrected = {"field": "to", "original": to_agent, "corrected": corrected_to} + to_agent = corrected_to + else: + # --- A2: to 必填(非回复场景) --- + to_agent = body.get("to", "").strip() + if not to_agent: + raise HTTPException(400, "`to` 必填") + + # --- A3: from != to 防自环 --- + if from_agent == to_agent: + raise HTTPException(400, "不能给自己发邮件") + + # --- A4: to 必须是有效 Agent --- + if to_agent not in valid_agents: + raise HTTPException(400, f"`to` 不是有效的 Agent: {to_agent}") + + # --- A10: 正文非空 --- + text = body.get("text", body.get("description", "")) or "" + if not text.strip(): + raise HTTPException(400, "邮件正文不能为空") mail_id = body.get("id", f"mail-{int(datetime.now().timestamp() * 1000)}") # 自动处理 conversation_id:有 in_reply_to 时继承原邮件的 conversation_id conversation_id = body.get("conversation_id") - in_reply_to = body.get("in_reply_to") - if not conversation_id and in_reply_to: - original = bb.get_task(in_reply_to) - if original: - try: - orig_meta = json.loads(original.must_haves) if original.must_haves else {} - conversation_id = orig_meta.get("conversation_id") - except Exception: - pass + if not conversation_id and original: + try: + orig_meta = json.loads(original.must_haves) if original.must_haves else {} + conversation_id = orig_meta.get("conversation_id") + except Exception: + pass if not conversation_id: conversation_id = f"conv-{int(datetime.now().timestamp() * 1000)}" @@ -220,15 +281,19 @@ async def send_mail(body: Dict[str, Any]): task = Task( id=mail_id, title=body.get("title", ""), - description=body.get("text", body.get("description", "")), - assignee=body.get("to"), - assigned_by=body.get("from", "user"), + description=text, + assignee=to_agent, + assigned_by=from_agent, must_haves=json.dumps(meta), task_type="mail", status="pending", ) bb.create_task(task) - return {"ok": True, "mail_id": task.id} + + result = {"ok": True, "mail_id": task.id} + if auto_corrected: + result["auto_corrected"] = auto_corrected + return result @router.patch("/{mail_id}")