db: AsyncSession = Depends(get_db), request: Request = None): """Clear All: revoke tokens at Google (best-effort) and remove all locally saved Gmail account rows. Audit trail preserved.""" rows = (await db.execute(select(GmailAccount).where( GmailAccount.user_id == user.id))).scalars().all() revoked = 0 for account in rows: token = account.oauth_token if token: if gmail_service.revoke_token(token.refresh_token or token.access_token): revoked += 1 await db.delete(account) # token cascades; related FKs are SET NULL await db.commit() await log_audit(db, user.id, "gmail.clear_all", "gmail_account", None, {"count": len(rows), "revoked_at_google": revoked}, request.client.host if request else None) return {"ok": True, "removed": len(rows), "revoked_at_google": revoked} @router.post("/accounts/import-config") async def import_client_config(file: UploadFile = File(...), user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Import & validate a Google OAuth client_secret JSON (never echoed back).""" from app.core.config import settings content = await file.read() if len(content) > 1024 * 512: raise HTTPException(413, "File too large (max 512 KB)") if not file.filename.lower().endswith(".json"): raise HTTPException(400, "Only .json configuration files are accepted") try: parsed = gmail_service.parse_client_json(content) except gmail_service.GmailOauthError as e: raise HTTPException(400, str(e)) row = OAuthClientConfig( name=parsed["project_id"] or file.filename[:100], user_id=user.id, client_id=parsed["client_id"], client_secret=parsed["client_secret"], # encrypted via property setter project_id=parsed["project_id"], redirect_uri=f"{settings.BASE_URL}/auth/google/callback", ) db.add(row) await db.commit() await log_audit(db, user.id, "oauth_config.imported", "oauth_client_config", row.id, {"project": parsed["project_id"]}, request.client.host if request else None) return {"ok": True, "id": row.id, "project_id": parsed["project_id"], "client_id_preview": parsed["client_id"][:12] + "…", "redirect_uri": row.redirect_uri} def _parse_bulk_smtp_lines(raw: str) -> list[tuple[str, str, str]]: """Parse free-form pasted lines into (raw_line, email, app_password) tuples. Accepts separators: ':' ',' '|' or whitespace. Blank lines / comments (#) skipped.""" import re as _re out = [] for line in raw.splitlines(): line = line.strip() if not line or line.startswith("#"): continue parts = _re.split(r"[:,|\s]+", line, maxsplit=1) if len(parts) != 2: out.append((line, "", "")) continue email, pwd = parts[0].strip(), parts[1].strip() out.append((line, email, pwd)) return out @router.post("/accounts/bulk-smtp") async def bulk_add_smtp_accounts(payload: BulkSmtpAdd,