from __future__ import annotations import time from typing import Any, Dict, Optional from fastapi import APIRouter, Depends, Header, HTTPException from app.config import get_settings from app.core.logger import get_logger from app.models.schemas import ( GmailAttachmentContent, GmailBatchDeleteRequest, GmailBatchModifyRequest, GmailComposeMessageRequest, GmailDraftCreateRequest, GmailDraftSendRequest, GmailDraftUpdateRequest, GmailFilterCreateRequest, GmailGenericResponse, GmailImportMessageRequest, GmailInsertMessageRequest, GmailLabelPatchRequest, GmailLabelRequest, GmailMessageModifyRequest, GmailRawMessageRequest, GmailRefreshResponse, GmailSendResponse, GmailSettingsUpdateRequest, GmailSmimeInfoRequest, GmailWatchRequest, ) from app.services.gmail_service import ( GmailAPIError, GmailCredentials, GmailService, ) router = APIRouter(prefix="/google/gmail", tags=["Gmail"]) _logger = get_logger(__name__) _settings = get_settings() _gmail_service = GmailService() def get_gmail_service() -> GmailService: return _gmail_service async def close_gmail_service() -> None: await _gmail_service.close() def _credentials( x_access_token: Optional[str] = Header(None, alias="X-Access-Token"), x_refresh_token: Optional[str] = Header(None, alias="X-Refresh-Token"), x_client_id: Optional[str] = Header(None, alias="X-Client-Id"), x_client_secret: Optional[str] = Header(None, alias="X-Client-Secret"), x_token_expires_at: Optional[float] = Header(None, alias="X-Token-Expires-At"), ) -> GmailCredentials: if not x_access_token: raise HTTPException( status_code=401, detail="X-Access-Token header is required.", ) return GmailCredentials( access_token=x_access_token, refresh_token=x_refresh_token, client_id=x_client_id, client_secret=x_client_secret, expires_at=x_token_expires_at, ) def _http_error(exc: GmailAPIError) -> HTTPException: return HTTPException(status_code=exc.status_code, detail=exc.message) def _elapsed_ms(start: float) -> float: return round((time.perf_counter() - start) * 1000, 2) def _ok( start: float, creds: GmailCredentials, data: Any = None, *, error: Optional[str] = None, ) -> GmailGenericResponse: return GmailGenericResponse( success=error is None, time_ms=_elapsed_ms(start), data=data, refreshed_access_token=creds.refreshed_access_token, error=error, ) def _user_id(delegate: str) -> str: return delegate or "me" def _attachment_content( message_id: str, attachment_id: str, raw: Dict[str, Any], ) -> GmailAttachmentContent: return GmailAttachmentContent( message_id=message_id, attachment_id=attachment_id, filename=raw.get("filename", ""), mime_type=raw.get("mimeType", ""), data_base64=raw.get("data", ""), size_bytes=int(raw.get("size", 0) or 0), ) # --------------------------------------------------------------------------- # Service metadata & token lifecycle # --------------------------------------------------------------------------- @router.get("/scopes", response_model=GmailGenericResponse, summary="List all supported Gmail OAuth scopes") async def list_scopes( service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() scopes = await service.list_available_scopes() _logger.info("Listed %d Gmail scopes (%.2fms)", len(scopes), _elapsed_ms(start)) return GmailGenericResponse(success=True, time_ms=_elapsed_ms(start), data=scopes) @router.post("/token/refresh", response_model=GmailRefreshResponse, summary="Refresh an access token (stateless, returned to the client)") async def token_refresh( body: Dict[str, str], service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() client_id = body.get("client_id", "") client_secret = body.get("client_secret", "") refresh_token = body.get("refresh_token", "") if not (client_id and client_secret and refresh_token): raise HTTPException( status_code=400, detail="client_id, client_secret and refresh_token are required.", ) try: tokens = await service.refresh_access_token( client_id=client_id, client_secret=client_secret, refresh_token=refresh_token, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail token refreshed (%.2fms)", _elapsed_ms(start)) return GmailRefreshResponse( success=True, access_token=tokens.get("access_token"), expires_in=int(tokens.get("expires_in", 0)), token_type=tokens.get("token_type", "Bearer"), scope=tokens.get("scope"), ) # --------------------------------------------------------------------------- # Profile # --------------------------------------------------------------------------- @router.get("/profile", response_model=GmailGenericResponse, summary="Get the authenticated user's Gmail profile") async def get_profile( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.get_profile(creds) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail profile fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) # --------------------------------------------------------------------------- # Watch / Stop / History (push notifications & change history) # --------------------------------------------------------------------------- @router.post("/watch", response_model=GmailGenericResponse, summary="Set up push notifications for mailbox changes (users.watch)") async def watch_mailbox( body: GmailWatchRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.watch( creds, user_id=body.user_id, topic_name=body.topic_name, label_ids=body.label_ids, label_filter_action=body.label_filter_action, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail watch enabled for %s (%.2fms)", body.user_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/stop", response_model=GmailGenericResponse, summary="Stop receiving push notifications (users.stop)") async def stop_watch_mailbox( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.stop_watch(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail watch stopped (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, {"stopped": True}) @router.get("/history", response_model=GmailGenericResponse, summary="List mailbox change history (users.history.list)") async def list_history( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), start_history_id: int = ..., label_id: Optional[str] = None, history_types: Optional[str] = None, max_results: Optional[int] = None, page_token: Optional[str] = None, user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_history( creds, user_id=user_id or "me", start_history_id=start_history_id, label_id=label_id, history_types=history_types.split(",") if history_types else None, max_results=max_results, page_token=page_token, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail history listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) # --------------------------------------------------------------------------- # Messages # --------------------------------------------------------------------------- @router.get("/messages", response_model=GmailGenericResponse, summary="List messages (supports Gmail search query and pagination)") async def list_messages( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), q: Optional[str] = None, label_ids: Optional[str] = None, max_results: Optional[int] = None, page_token: Optional[str] = None, include_spam_trash: bool = False, user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_messages( creds, user_id=user_id or "me", q=q, label_ids=label_ids.split(",") if label_ids else None, max_results=max_results, page_token=page_token, include_spam_trash=include_spam_trash, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail messages listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/messages/{message_id}", response_model=GmailGenericResponse, summary="Get a single message (format: minimal, metadata, full, raw)") async def get_message( message_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), format: str = "full", metadata_headers: Optional[str] = None, user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_message( creds, message_id, user_id=user_id or "me", format=format, metadata_headers=metadata_headers.split(",") if metadata_headers else None, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s fetched (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/messages/{message_id}/parsed", response_model=GmailGenericResponse, summary="Get a message parsed into headers, bodies and attachment metadata") async def get_parsed_message( message_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: parsed = await service.get_parsed_message(creds, message_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s parsed (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, parsed.model_dump()) @router.get("/messages/{message_id}/attachments/{attachment_id}", response_model=GmailGenericResponse, summary="Get a message attachment's content (base64url)") async def get_attachment( message_id: str, attachment_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: raw = await service.get_attachment( creds, message_id, attachment_id, user_id=user_id or "me" ) except GmailAPIError as exc: raise _http_error(exc) from exc content = _attachment_content(message_id, attachment_id, raw) _logger.info( "Gmail attachment %s fetched for message %s (%.2fms)", attachment_id, message_id, _elapsed_ms(start), ) return _ok(start, creds, content.model_dump()) @router.post("/messages/send", response_model=GmailSendResponse, summary="Send an email built from a structured compose request") async def send_composed( body: GmailComposeMessageRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.send_composed(creds, body, user_id=_user_id(body.delegate)) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail email sent (%.2fms)", _elapsed_ms(start)) return GmailSendResponse( success=True, time_ms=_elapsed_ms(start), message_id=data.get("id"), thread_id=data.get("threadId"), label_ids=data.get("labelIds"), refreshed_access_token=creds.refreshed_access_token, data=data, ) @router.post("/messages/send/raw", response_model=GmailSendResponse, summary="Send a pre-encoded RFC 2822 message") async def send_raw( body: GmailRawMessageRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.send_raw( creds, body.raw, user_id=_user_id(body.delegate), thread_id=body.thread_id, labels=body.labels, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail raw message sent (%.2fms)", _elapsed_ms(start)) return GmailSendResponse( success=True, time_ms=_elapsed_ms(start), message_id=data.get("id"), thread_id=data.get("threadId"), label_ids=data.get("labelIds"), refreshed_access_token=creds.refreshed_access_token, data=data, ) @router.post("/messages", response_model=GmailSendResponse, summary="Insert a message directly into the mailbox (does not send)") async def insert_message( body: GmailInsertMessageRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.insert_message( creds, body.raw, user_id=_user_id(body.delegate), label_ids=body.label_ids, internal_date_source=body.internal_date_source, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message inserted (%.2fms)", _elapsed_ms(start)) return GmailSendResponse( success=True, time_ms=_elapsed_ms(start), message_id=data.get("id"), thread_id=data.get("threadId"), refreshed_access_token=creds.refreshed_access_token, data=data, ) @router.post("/messages/import", response_model=GmailSendResponse, summary="Import an RFC 2822 message into the mailbox (users.messages.import)") async def import_message( body: GmailImportMessageRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.import_message( creds, body.raw, user_id=_user_id(body.delegate), label_ids=body.label_ids, internal_date_source=body.internal_date_source, never_mark_spam=body.never_mark_spam, process_for_calendar=body.process_for_calendar, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message imported (%.2fms)", _elapsed_ms(start)) return GmailSendResponse( success=True, time_ms=_elapsed_ms(start), message_id=data.get("id"), thread_id=data.get("threadId"), refreshed_access_token=creds.refreshed_access_token, data=data, ) @router.post("/messages/batchModify", response_model=GmailGenericResponse, summary="Add/remove labels across many messages") async def batch_modify( body: GmailBatchModifyRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: await service.batch_modify( creds, body.ids, user_id="me", add_label_ids=body.add_label_ids, remove_label_ids=body.remove_label_ids, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail batch modify on %d messages (%.2fms)", len(body.ids), _elapsed_ms(start)) return _ok(start, creds, {"modified": len(body.ids)}) @router.post("/messages/batchDelete", response_model=GmailGenericResponse, summary="Permanently delete many messages") async def batch_delete( body: GmailBatchDeleteRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: await service.batch_delete(creds, body.ids, user_id="me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail batch delete on %d messages (%.2fms)", len(body.ids), _elapsed_ms(start)) return _ok(start, creds, {"deleted": len(body.ids)}) @router.post("/messages/{message_id}/modify", response_model=GmailGenericResponse, summary="Add/remove labels on a single message (e.g. mark read/unread)") async def modify_message( message_id: str, body: GmailMessageModifyRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.modify_message( creds, message_id, user_id=user_id or "me", add_label_ids=body.add_label_ids, remove_label_ids=body.remove_label_ids, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s modified (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/messages/{message_id}/trash", response_model=GmailGenericResponse, summary="Move a message to trash") async def trash_message( message_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.trash_message(creds, message_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s trashed (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/messages/{message_id}/untrash", response_model=GmailGenericResponse, summary="Restore a message from trash") async def untrash_message( message_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.untrash_message(creds, message_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s untrashed (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/messages/{message_id}", response_model=GmailGenericResponse, summary="Permanently delete a message") async def delete_message( message_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_message(creds, message_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail message %s deleted (%.2fms)", message_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": message_id}) # --------------------------------------------------------------------------- # Drafts # --------------------------------------------------------------------------- @router.get("/drafts", response_model=GmailGenericResponse, summary="List drafts") async def list_drafts( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), max_results: Optional[int] = None, page_token: Optional[str] = None, q: Optional[str] = None, user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_drafts( creds, user_id=user_id or "me", max_results=max_results, page_token=page_token, q=q, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail drafts listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/drafts/{draft_id}", response_model=GmailGenericResponse, summary="Get a single draft") async def get_draft( draft_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), format: str = "full", user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_draft(creds, draft_id, user_id=user_id or "me", format=format) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail draft %s fetched (%.2fms)", draft_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/drafts", response_model=GmailGenericResponse, summary="Create a draft from a pre-encoded RFC 2822 message") async def create_draft( body: GmailDraftCreateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.create_draft( creds, body.raw, user_id=_user_id(body.delegate), thread_id=body.thread_id, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail draft created (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/drafts/{draft_id}", response_model=GmailGenericResponse, summary="Replace an existing draft") async def update_draft( draft_id: str, body: GmailDraftUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.update_draft( creds, draft_id, body.raw, user_id=_user_id(body.delegate), thread_id=body.thread_id, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail draft %s updated (%.2fms)", draft_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/drafts/send", response_model=GmailSendResponse, summary="Send an existing draft") async def send_draft( body: GmailDraftSendRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), ): start = time.perf_counter() try: data = await service.send_draft(creds, body.draft_id, user_id=_user_id(body.delegate)) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail draft %s sent (%.2fms)", body.draft_id, _elapsed_ms(start)) return GmailSendResponse( success=True, time_ms=_elapsed_ms(start), message_id=data.get("id"), thread_id=data.get("threadId"), refreshed_access_token=creds.refreshed_access_token, data=data, ) @router.delete("/drafts/{draft_id}", response_model=GmailGenericResponse, summary="Delete a draft") async def delete_draft( draft_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_draft(creds, draft_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail draft %s deleted (%.2fms)", draft_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": draft_id}) # --------------------------------------------------------------------------- # Threads # --------------------------------------------------------------------------- @router.get("/threads", response_model=GmailGenericResponse, summary="List threads") async def list_threads( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), q: Optional[str] = None, label_ids: Optional[str] = None, max_results: Optional[int] = None, page_token: Optional[str] = None, user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_threads( creds, user_id=user_id or "me", q=q, label_ids=label_ids.split(",") if label_ids else None, max_results=max_results, page_token=page_token, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail threads listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/threads/{thread_id}", response_model=GmailGenericResponse, summary="Get a single thread with all its messages") async def get_thread( thread_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), format: str = "full", user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_thread(creds, thread_id, user_id=user_id or "me", format=format) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail thread %s fetched (%.2fms)", thread_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/threads/{thread_id}/modify", response_model=GmailGenericResponse, summary="Add/remove labels on all messages in a thread") async def modify_thread( thread_id: str, body: GmailMessageModifyRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.modify_thread( creds, thread_id, user_id=user_id or "me", add_label_ids=body.add_label_ids, remove_label_ids=body.remove_label_ids, ) except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail thread %s modified (%.2fms)", thread_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/threads/{thread_id}/trash", response_model=GmailGenericResponse, summary="Move all messages in a thread to trash") async def trash_thread( thread_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.trash_thread(creds, thread_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail thread %s trashed (%.2fms)", thread_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/threads/{thread_id}/untrash", response_model=GmailGenericResponse, summary="Restore all messages in a thread from trash") async def untrash_thread( thread_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.untrash_thread(creds, thread_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail thread %s untrashed (%.2fms)", thread_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/threads/{thread_id}", response_model=GmailGenericResponse, summary="Permanently delete a thread") async def delete_thread( thread_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_thread(creds, thread_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail thread %s deleted (%.2fms)", thread_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": thread_id}) # --------------------------------------------------------------------------- # Labels # --------------------------------------------------------------------------- @router.get("/labels", response_model=GmailGenericResponse, summary="List all labels") async def list_labels( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_labels(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail labels listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/labels/{label_id}", response_model=GmailGenericResponse, summary="Get a single label") async def get_label( label_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_label(creds, label_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail label %s fetched (%.2fms)", label_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/labels", response_model=GmailGenericResponse, summary="Create a user label") async def create_label( body: GmailLabelRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.create_label(creds, body, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail label '%s' created (%.2fms)", body.name, _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/labels/{label_id}", response_model=GmailGenericResponse, summary="Update a user label") async def update_label( label_id: str, body: GmailLabelRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_label(creds, label_id, body, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail label %s updated (%.2fms)", label_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.patch("/labels/{label_id}", response_model=GmailGenericResponse, summary="Partially update a user label (users.labels.patch)") async def patch_label( label_id: str, body: GmailLabelPatchRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() payload = body.model_dump(exclude_none=True) if not payload: raise HTTPException( status_code=400, detail="At least one label field (name, label_list_visibility, message_list_visibility, color) is required.", ) try: data = await service.patch_label(creds, label_id, payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail label %s patched (%.2fms)", label_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/labels/{label_id}", response_model=GmailGenericResponse, summary="Delete a user label") async def delete_label( label_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_label(creds, label_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail label %s deleted (%.2fms)", label_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": label_id}) # --------------------------------------------------------------------------- # Settings # --------------------------------------------------------------------------- @router.get("/settings/auto-forwarding", response_model=GmailGenericResponse, summary="Get auto-forwarding settings") async def get_auto_forwarding( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_auto_forwarding(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail auto-forwarding fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/settings/auto-forwarding", response_model=GmailGenericResponse, summary="Update auto-forwarding settings") async def update_auto_forwarding( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_auto_forwarding(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail auto-forwarding updated (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/vacation", response_model=GmailGenericResponse, summary="Get vacation responder settings") async def get_vacation( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_vacation(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail vacation settings fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/settings/vacation", response_model=GmailGenericResponse, summary="Update vacation responder settings") async def update_vacation( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_vacation(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail vacation settings updated (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/imap", response_model=GmailGenericResponse, summary="Get IMAP settings") async def get_imap( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_imap(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail IMAP settings fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/settings/imap", response_model=GmailGenericResponse, summary="Update IMAP settings") async def update_imap( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_imap(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail IMAP settings updated (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/pop", response_model=GmailGenericResponse, summary="Get POP settings") async def get_pop( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_pop(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail POP settings fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/settings/pop", response_model=GmailGenericResponse, summary="Update POP settings") async def update_pop( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_pop(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail POP settings updated (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/language", response_model=GmailGenericResponse, summary="Get language settings") async def get_language( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_language(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail language settings fetched (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.put("/settings/language", response_model=GmailGenericResponse, summary="Update language settings") async def update_language( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_language(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail language settings updated (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/filters", response_model=GmailGenericResponse, summary="List all filters") async def list_filters( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_filters(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail filters listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/filters/{filter_id}", response_model=GmailGenericResponse, summary="Get a single filter") async def get_filter( filter_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_filter(creds, filter_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail filter %s fetched (%.2fms)", filter_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/settings/filters", response_model=GmailGenericResponse, summary="Create a filter") async def create_filter( body: GmailFilterCreateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.create_filter(creds, body, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail filter created (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/settings/filters/{filter_id}", response_model=GmailGenericResponse, summary="Delete a filter") async def delete_filter( filter_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_filter(creds, filter_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail filter %s deleted (%.2fms)", filter_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": filter_id}) @router.get("/settings/forwarding-addresses", response_model=GmailGenericResponse, summary="List forwarding addresses") async def list_forwarding_addresses( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_forwarding_addresses(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail forwarding addresses listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/forwarding-addresses/{forwarding_email}", response_model=GmailGenericResponse, summary="Get a forwarding address") async def get_forwarding_address( forwarding_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_forwarding_address(creds, forwarding_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail forwarding address %s fetched (%.2fms)", forwarding_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/settings/forwarding-addresses", response_model=GmailGenericResponse, summary="Create a forwarding address") async def create_forwarding_address( body: Dict[str, str], creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() forwarding_email = body.get("forwarding_email", "") if not forwarding_email: raise HTTPException(status_code=400, detail="forwarding_email is required.") try: data = await service.create_forwarding_address(creds, forwarding_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail forwarding address %s created (%.2fms)", forwarding_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/settings/forwarding-addresses/{forwarding_email}", response_model=GmailGenericResponse, summary="Delete a forwarding address") async def delete_forwarding_address( forwarding_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_forwarding_address(creds, forwarding_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail forwarding address %s deleted (%.2fms)", forwarding_email, _elapsed_ms(start)) return _ok(start, creds, {"deleted": forwarding_email}) @router.get("/settings/send-as", response_model=GmailGenericResponse, summary="List send-as aliases") async def list_send_as( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_send_as(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as aliases listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/send-as/{send_as_email}", response_model=GmailGenericResponse, summary="Get a send-as alias") async def get_send_as( send_as_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_send_as(creds, send_as_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as %s fetched (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/settings/send-as", response_model=GmailGenericResponse, summary="Create a send-as alias") async def create_send_as( body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.create_send_as(creds, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as created (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.patch("/settings/send-as/{send_as_email}", response_model=GmailGenericResponse, summary="Update a send-as alias") async def update_send_as( send_as_email: str, body: GmailSettingsUpdateRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.update_send_as(creds, send_as_email, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as %s updated (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/settings/send-as/{send_as_email}", response_model=GmailGenericResponse, summary="Delete a send-as alias") async def delete_send_as( send_as_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_send_as(creds, send_as_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as %s deleted (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, {"deleted": send_as_email}) @router.post("/settings/send-as/{send_as_email}/verify", response_model=GmailGenericResponse, summary="Send a verification email for a send-as alias (users.settings.sendAs.verify)") async def verify_send_as( send_as_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.verify_send_as(creds, send_as_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail send-as %s verification triggered (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, {"verification_sent": send_as_email}) @router.get("/settings/send-as/{send_as_email}/smime-info", response_model=GmailGenericResponse, summary="List S/MIME certificates for a send-as alias (smimeInfo.list)") async def list_smime_info( send_as_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_smime_info(creds, send_as_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail smimeInfo listed for %s (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/settings/send-as/{send_as_email}/smime-info", response_model=GmailGenericResponse, summary="Insert an S/MIME certificate for a send-as alias (smimeInfo.insert)") async def insert_smime_info( send_as_email: str, body: GmailSmimeInfoRequest, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.insert_smime_info(creds, send_as_email, body.payload, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail smimeInfo inserted for %s (%.2fms)", send_as_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}", response_model=GmailGenericResponse, summary="Get a single S/MIME certificate (smimeInfo.get)") async def get_smime_info( send_as_email: str, smime_info_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail smimeInfo %s fetched (%.2fms)", smime_info_id, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}", response_model=GmailGenericResponse, summary="Delete an S/MIME certificate (smimeInfo.delete)") async def delete_smime_info( send_as_email: str, smime_info_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail smimeInfo %s deleted (%.2fms)", smime_info_id, _elapsed_ms(start)) return _ok(start, creds, {"deleted": smime_info_id}) @router.post("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}/setDefault", response_model=GmailGenericResponse, summary="Set an S/MIME certificate as the default for a send-as alias (smimeInfo.setDefault)") async def set_default_smime_info( send_as_email: str, smime_info_id: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.set_default_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail smimeInfo %s set default (%.2fms)", smime_info_id, _elapsed_ms(start)) return _ok(start, creds, {"default_set": smime_info_id}) @router.get("/settings/delegates", response_model=GmailGenericResponse, summary="List delegates") async def list_delegates( creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.list_delegates(creds, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail delegates listed (%.2fms)", _elapsed_ms(start)) return _ok(start, creds, data) @router.get("/settings/delegates/{delegate_email}", response_model=GmailGenericResponse, summary="Get a delegate") async def get_delegate( delegate_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: data = await service.get_delegate(creds, delegate_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail delegate %s fetched (%.2fms)", delegate_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.post("/settings/delegates", response_model=GmailGenericResponse, summary="Create a delegate") async def create_delegate( body: Dict[str, str], creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() delegate_email = body.get("delegate_email", "") if not delegate_email: raise HTTPException(status_code=400, detail="delegate_email is required.") try: data = await service.create_delegate(creds, delegate_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail delegate %s created (%.2fms)", delegate_email, _elapsed_ms(start)) return _ok(start, creds, data) @router.delete("/settings/delegates/{delegate_email}", response_model=GmailGenericResponse, summary="Delete a delegate") async def delete_delegate( delegate_email: str, creds: GmailCredentials = Depends(_credentials), service: GmailService = Depends(get_gmail_service), user_id: Optional[str] = None, ): start = time.perf_counter() try: await service.delete_delegate(creds, delegate_email, user_id=user_id or "me") except GmailAPIError as exc: raise _http_error(exc) from exc _logger.info("Gmail delegate %s deleted (%.2fms)", delegate_email, _elapsed_ms(start)) return _ok(start, creds, {"deleted": delegate_email})