You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
4.3 KiB
Python

# backend/app/connectors/mail/normalize.py
# provider(mock/gmail/imap) → 내부 email 모델(phase-9) 정규화 + 멱등 upsert.
import base64
import uuid
from sqlmodel import Session, select
from ...models import ConnectorAccount, Email, ExternalLink
from ..base import NormalizedRecord, RawRecord
# connector_account id ↔ mail_account id 매핑(시드 정합)
_ACCOUNT_KEY = {
"ca-mail-work": "work",
"ca-mail-personal": "personal",
"ca-mail-side": "side",
}
# Gmail labelId → 내부 한국어 라벨
_LABEL_MAP = {
"CATEGORY_PERSONAL": "개인",
"CATEGORY_PROMOTIONS": "프로모션",
"CATEGORY_UPDATES": "업데이트",
"CATEGORY_FORUMS": "포럼",
"CATEGORY_SOCIAL": "소셜",
"IMPORTANT": "중요",
}
def _gmail_headers(p: dict) -> dict:
return {h["name"].lower(): h["value"] for h in p.get("payload", {}).get("headers", [])}
def _gmail_body(p: dict) -> list:
"""MIME 파트에서 text/plain 본문을 base64url 디코딩(없으면 snippet)."""
payload = p.get("payload", {})
parts = payload.get("parts") or [payload]
for part in parts:
if part.get("mimeType", "").startswith("text/"):
data = part.get("body", {}).get("data")
if data:
try:
text = base64.urlsafe_b64decode(data + "===").decode("utf-8", "ignore")
return [ln for ln in text.split("\n") if ln.strip()]
except Exception:
pass
snip = p.get("snippet", "")
return [snip] if snip else []
def _gmail_labels(label_ids: list) -> list:
return [_LABEL_MAP[lid] for lid in label_ids if lid in _LABEL_MAP]
def _gmail_has_attach(p: dict) -> bool:
for part in p.get("payload", {}).get("parts", []) or []:
if part.get("filename"):
return True
return False
def normalize_email(
account: ConnectorAccount, raw: RawRecord, provider: str = "mock"
) -> NormalizedRecord:
p = raw.payload
if provider == "gmail":
headers = _gmail_headers(p)
label_ids = p.get("labelIds", [])
fields = {
"account": _ACCOUNT_KEY.get(account.id, "work"),
"from_key": headers.get("from", ""),
"to": headers.get("to", "나"),
"subject": headers.get("subject", "(제목 없음)"),
"body": _gmail_body(p),
"labels": _gmail_labels(label_ids),
"read": "UNREAD" not in label_ids,
"starred": "STARRED" in label_ids,
"has_attach": _gmail_has_attach(p),
"preview": p.get("snippet", ""),
"date": "방금",
"ai_json": {}, # phase-9 메일 분석(로컬 LLM)이 후처리로 채움
}
else: # mock(mail-data.js shape) / imap
body = p.get("body", [])
fields = {
"account": _ACCOUNT_KEY.get(account.id, p.get("account", "work")),
"from_key": p.get("from", p.get("from_key", "")),
"to": p.get("to", "나"),
"subject": p.get("subject", ""),
"body": body if isinstance(body, list) else [body],
"labels": p.get("labels", []),
"read": p.get("read", False),
"starred": p.get("starred", False),
"has_attach": p.get("hasAttach", p.get("has_attach", False)),
"preview": p.get("preview", ""),
"date": p.get("date", "방금"),
"ai_json": p.get("ai", {}) or {},
}
return NormalizedRecord(
entity_type="email",
external_id=raw.external_id,
fields=fields,
etag=raw.etag,
external_updated_at=raw.external_updated_at,
)
def upsert_email(session: Session, norm: NormalizedRecord) -> tuple[str, bool]:
link = session.exec(
select(ExternalLink).where(
ExternalLink.external_id == norm.external_id,
ExternalLink.entity_type == "email",
)
).first()
if link: # 이미 존재 → update(멱등)
e = session.get(Email, link.entity_id)
if e:
for k, v in norm.fields.items():
if v is not None and hasattr(e, k):
setattr(e, k, v)
session.add(e)
return e.id, False
eid = "m-" + uuid.uuid4().hex[:8]
e = Email(id=eid, **{k: v for k, v in norm.fields.items() if v is not None})
session.add(e)
return eid, True