"""Inbox helpers — attachment loading, resume text extraction, read-status sync.""" from __future__ import annotations import base64 import os from pathlib import Path from urllib.parse import quote import httpx from dotenv import load_dotenv from inbox.models import Inbox_Messages from job.candidate.views import FileRead load_dotenv() EMAIL_URL=os.getenv("EMAIL_URL") EMAIL_API_TOKEN=os.getenv("EMAIL_API_TOKEN") _ATTACHMENTS_DIR=Path(__file__).resolve().parent/"decoded_attachments" async def fetch_read_status_delta(folder, since=None, limit=1000, max_pages=10, token=None): """GET /sync/read-status -> the raw round dict.""" if not EMAIL_URL: raise RuntimeError("EMAIL_URL must be set") auth_token=token or EMAIL_API_TOKEN if not auth_token: raise RuntimeError("EMAIL_API_TOKEN must be set") params={"folder":folder,"limit":limit,"max_pages":max_pages} if since: params["since"]=since async with httpx.AsyncClient(timeout=15.0) as client: response=await client.get( f"{EMAIL_URL.rstrip('/')}/sync/read-status", params=params, headers={"Authorization":f"Bearer {auth_token}"}, ) if response.status_code>=400: raise httpx.HTTPStatusError( response.text, request=response.request, response=response, ) return response.json() async def fetch_message_read_status(message_id, token=None): """GET /sync/read-status/message/{id} -> record dict, or None on 404.""" if not EMAIL_URL: raise RuntimeError("EMAIL_URL must be set") auth_token=token or EMAIL_API_TOKEN if not auth_token: raise RuntimeError("EMAIL_API_TOKEN must be set") encoded_id=quote(str(message_id),safe="") async with httpx.AsyncClient(timeout=15.0) as client: response=await client.get( f"{EMAIL_URL.rstrip('/')}/sync/read-status/message/{encoded_id}", headers={"Authorization":f"Bearer {auth_token}"}, ) if response.status_code==404: return None if response.status_code>=400: raise httpx.HTTPStatusError( response.text, request=response.request, response=response, ) return response.json() def resolve_attachment_path(path_str:str) -> Path: """Prefer stored path; fall back to basename under decoded_attachments.""" path=Path(path_str.strip()) if path.is_file(): return path fallback=_ATTACHMENTS_DIR/path.name if fallback.is_file(): return fallback return path def load_message_files(message:Inbox_Messages) -> list[dict]: if not message.file_path: return [] files=[] for path_str in message.file_path.split(","): path=resolve_attachment_path(path_str) if not path.is_file(): continue try: raw=path.read_bytes() except OSError: continue files.append({ "file_name":path.name, "content_base64":base64.b64encode(raw).decode("ascii"), "size":len(raw), }) return files async def extract_resume_text(file_paths:list[str]) -> tuple[str,str]: candidates=[resolve_attachment_path(p) for p in (file_paths or []) if p and p.strip()] existing=[p for p in candidates if p.is_file() and p.suffix.lower()==".pdf"] if not existing: return "","no PDF attachment to extract (.doc/.docx not supported)" texts=[] errors=[] for path in existing: try: raw=path.read_bytes() result=await FileRead(session=None,filename=path.name,file=raw).read_file() text=(result.get("text") or "").strip() if text: texts.append(text) except Exception as exc: errors.append(f"{path.name}: {exc}") if not texts: return "","; ".join(errors) if errors else "no text extracted from PDF" return "\n\n---\n\n".join(texts),""