root 1 settimana fa
parent
commit
512420fef8
2 ha cambiato i file con 63 aggiunte e 20 eliminazioni
  1. 4 2
      app/api/router.py
  2. 59 18
      app/services/email_authorizations_service.py

+ 4 - 2
app/api/router.py

@@ -245,6 +245,7 @@ async def email_authorizations_fetch_email(
     bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
     sentDate: str = Query(..., description="发件日期, UTC时间, 格式: yyyy-mm-dd hh:mm:ss"),
     expiry: int = Query(300, description="邮件有效期, 单位秒, 从sentDate 开始算起"),
+    onlyText: bool = Query(True, description="是否纯文本"),
     db: AsyncSession = Depends(get_db)
 ):
     auth = await EmailAuthorizationService.get_by_email(db, email)
@@ -257,7 +258,7 @@ async def email_authorizations_fetch_email(
         body_keywords=bodyKeywords,
         sent_date=sentDate,
         expiry=expiry,
-        only_text=True
+        only_text=onlyText
     )
     return success(data={"body": result})
 
@@ -269,6 +270,7 @@ async def email_authorizations_fetch_email_from_topn(
     subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
     bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
     top: int = Query(10, description="指定从最近几封邮件读取"),
+    onlyText: bool = Query(True, description="是否纯文本"),
     db: AsyncSession = Depends(get_db)
 ):
     auth = await EmailAuthorizationService.get_by_email(db, email)
@@ -279,7 +281,7 @@ async def email_authorizations_fetch_email_from_topn(
         subject_keywords=subjectKeywords,
         body_keywords=bodyKeywords,
         top=top,
-        only_text=True
+        only_text=onlyText
     )
     return success(data={"body": result})
 

+ 59 - 18
app/services/email_authorizations_service.py

@@ -7,6 +7,7 @@ import email
 import asyncio
 import re
 import time
+import base64
 from datetime import datetime, timedelta, timezone
 import email.policy
 from email.message import EmailMessage
@@ -1009,30 +1010,70 @@ class EmailAuthorizationService:
 
     @staticmethod
     def _extract_body(msg, only_text: bool = True) -> str:
-        # 纯 CPU 计算,不需要 async,保留原样
-        body_parts = []
-        if msg.is_multipart():
-            for part in msg.walk():
-                ctype = part.get_content_type()
-                if only_text and ctype != "text/plain":
-                    continue
-                if not only_text and ctype not in ["text/plain", "text/html"]:
-                    continue
-                if part.get("Content-Disposition"):
+        text_parts = []
+        image_parts = []
+
+        # 统一处理 multipart 和单体邮件
+        parts = msg.walk() if msg.is_multipart() else [msg]
+
+        for part in parts:
+            # 安全获取属性,转为小写方便判断
+            ctype = str(part.get_content_type()).lower()
+            disposition = str(part.get("Content-Disposition")).lower()
+            content_id = str(part.get("Content-ID")).lower()
+            filename = str(part.get_filename() or "").lower()
+
+            # 1. 提取文字内容 (同时允许 text/plain 和 text/html,防止 VFS 这种只有 HTML 的邮件导致内容为空)
+            if ctype in ["text/plain", "text/html"]:
+                # 忽略明确被标记为真正附件的文本
+                if "attachment" in disposition:
                     continue
+                
                 charset = part.get_content_charset() or "utf-8"
                 try:
                     payload = part.get_payload(decode=True)
                     if payload:
                         text = payload.decode(charset, errors="ignore")
-                        body_parts.append(text)
+                        text_parts.append(text)
                 except Exception:
                     continue
-        else:
-            charset = msg.get_content_charset() or "utf-8"
-            payload = msg.get_payload(decode=True)
-            if payload:
-                body_parts.append(payload.decode(charset, errors="ignore"))
+
+            # 2. 提取图片内容 (当 onlyText=False 时触发)
+            elif not only_text:
+                # 兼容 VFS 等发件系统的恶心格式:判断是否为图片
+                # a) 标准的 image/ 开头
+                # b) 带有 Content-ID (如 cid:otp_image) 且不是文字
+                # c) 文件名以图片格式结尾
+                is_image = (
+                    ctype.startswith("image/") or 
+                    (content_id and not ctype.startswith("text/")) or 
+                    filename.endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))
+                )
+                
+                if is_image:
+                    try:
+                        payload = part.get_payload(decode=True)
+                        if payload:
+                            # 如果 ctype 不规范 (比如 application/octet-stream),强制转为 image/png 供大模型使用
+                            img_type = ctype if ctype.startswith("image/") else "image/png"
+                            
+                            b64_image = base64.b64encode(payload).decode("utf-8")
+                            image_data_uri = f"data:{img_type};base64,{b64_image}"
+                            image_parts.append(image_data_uri)
+                    except Exception:
+                        continue
+
+        # 3. 处理文本部分(保留你的原始清理逻辑)
+        raw_text = "\n".join(text_parts)
+        cleaned_text = re.sub(r"\s+", " ", raw_text.strip())
+
+        # 4. 合并文本和图片,用换行符切分
+        final_parts = []
+        if cleaned_text:
+            final_parts.append(cleaned_text)
         
-        body = "\n".join(body_parts)
-        return re.sub(r"\s+", " ", body.strip())
+        if image_parts:
+            final_parts.extend(image_parts)
+
+        # 最终返回一个字符串:文本在前,图片数据在后
+        return "\n".join(final_parts)