llm-ready-data / app /api /v1 /gmail.py
validops-east-1's picture
feat: complete gmail and sheets api method coverage
3d1f304
Raw
History Blame Contribute Delete
56.1 kB
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})