Spaces:
Sleeping
Sleeping
feat:Added gmail api
Browse files- src/core/config.py +11 -2
- src/main.py +1 -2
- src/profile/router.py +61 -13
- src/profile/schemas.py +13 -0
- src/profile/service.py +51 -4
- src/profile/utils.py +69 -0
src/core/config.py
CHANGED
|
@@ -33,6 +33,14 @@ class Settings(BaseSettings):
|
|
| 33 |
FERNET_KEY: str
|
| 34 |
VERIFICATION_BASE_URL: str
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@computed_field
|
| 37 |
@property
|
| 38 |
def DATABASE_URL(self) -> PostgresDsn:
|
|
@@ -45,8 +53,9 @@ class Settings(BaseSettings):
|
|
| 45 |
"""Async DB URL"""
|
| 46 |
return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}/{self.POSTGRES_DB}"
|
| 47 |
|
| 48 |
-
model_config = SettingsConfigDict(
|
| 49 |
-
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
settings = Settings()
|
|
|
|
| 33 |
FERNET_KEY: str
|
| 34 |
VERIFICATION_BASE_URL: str
|
| 35 |
|
| 36 |
+
GOOGLE_CLIENT_ID: str
|
| 37 |
+
GOOGLE_CLIENT_SECRET: str
|
| 38 |
+
GOOGLE_REDIRECT_URI: str
|
| 39 |
+
|
| 40 |
+
AUTH_BASE: str = "https://accounts.google.com/o/oauth2/v2/auth"
|
| 41 |
+
TOKEN_URL: str = "https://oauth2.googleapis.com/token"
|
| 42 |
+
GMAIL_SEND_SCOPE: str = "https://www.googleapis.com/auth/gmail.send"
|
| 43 |
+
|
| 44 |
@computed_field
|
| 45 |
@property
|
| 46 |
def DATABASE_URL(self) -> PostgresDsn:
|
|
|
|
| 53 |
"""Async DB URL"""
|
| 54 |
return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}/{self.POSTGRES_DB}"
|
| 55 |
|
| 56 |
+
model_config = SettingsConfigDict(
|
| 57 |
+
env_file=".env", case_sensitive=False, env_file_encoding="utf-8"
|
| 58 |
+
)
|
| 59 |
|
| 60 |
|
| 61 |
settings = Settings()
|
src/main.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
-
from src.assets.router import router as assets
|
| 2 |
from src.profile.router import router as profile
|
| 3 |
from fastapi import FastAPI
|
| 4 |
|
| 5 |
from src.core.database import init_db
|
| 6 |
from src.home.router import router as home_router
|
| 7 |
from src.auth.router import router as auth_router
|
| 8 |
-
|
| 9 |
|
| 10 |
app = FastAPI(title="Yuvabe App API")
|
| 11 |
|
|
|
|
|
|
|
| 1 |
from src.profile.router import router as profile
|
| 2 |
from fastapi import FastAPI
|
| 3 |
|
| 4 |
from src.core.database import init_db
|
| 5 |
from src.home.router import router as home_router
|
| 6 |
from src.auth.router import router as auth_router
|
| 7 |
+
|
| 8 |
|
| 9 |
app = FastAPI(title="Yuvabe App API")
|
| 10 |
|
src/profile/router.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi.routing import APIRouter
|
| 2 |
from src.core.database import get_async_session
|
| 3 |
from src.auth.utils import get_current_user
|
|
@@ -6,23 +13,66 @@ from sqlalchemy.ext.asyncio.session import AsyncSession
|
|
| 6 |
from fastapi.params import Depends
|
| 7 |
from .schemas import UpdateProfileRequest
|
| 8 |
from src.profile.service import update_user_profile
|
| 9 |
-
from fastapi import APIRouter, Depends
|
| 10 |
-
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 11 |
-
from src.core.database import get_async_session
|
| 12 |
-
from src.auth.utils import get_current_user
|
| 13 |
-
from src.assets.schemas import BaseResponse
|
| 14 |
-
from src.assets.service import list_user_assets
|
| 15 |
-
from src.leave.utils import send_email
|
| 16 |
-
from fastapi import APIRouter, Depends, HTTPException
|
| 17 |
from sqlmodel import select
|
| 18 |
-
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 19 |
-
from src.auth.utils import get_current_user
|
| 20 |
from src.core.models import Users, Teams, Roles, UserTeamsRole
|
| 21 |
-
from fastapi import BackgroundTasks
|
| 22 |
|
| 23 |
|
| 24 |
router = APIRouter(prefix="/profile", tags=["Profile"])
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
@router.get("/", response_model=BaseResponse)
|
| 28 |
async def get_assets(
|
|
@@ -56,7 +106,6 @@ async def update_profile(
|
|
| 56 |
return {"code": 200, "data": result}
|
| 57 |
|
| 58 |
|
| 59 |
-
|
| 60 |
@router.get("/contacts", response_model=BaseResponse)
|
| 61 |
async def get_leave_contacts(
|
| 62 |
current_user=Depends(get_current_user),
|
|
@@ -135,4 +184,3 @@ async def send_leave_email(
|
|
| 135 |
background.add_task(send_email, to_email, subject, body, cc, from_email)
|
| 136 |
|
| 137 |
return BaseResponse(code=200, message="Leave request sent", data=None)
|
| 138 |
-
|
|
|
|
| 1 |
+
from src.profile.service import send_email_service
|
| 2 |
+
from src.profile.schemas import SendMailRequest
|
| 3 |
+
from src.profile.utils import build_auth_url
|
| 4 |
+
from src.profile.utils import exchange_code_for_tokens
|
| 5 |
+
from src.profile.service import USER_TOKEN_STORE
|
| 6 |
+
from src.profile.utils import send_email
|
| 7 |
+
from src.profile.service import list_user_assets
|
| 8 |
from fastapi.routing import APIRouter
|
| 9 |
from src.core.database import get_async_session
|
| 10 |
from src.auth.utils import get_current_user
|
|
|
|
| 13 |
from fastapi.params import Depends
|
| 14 |
from .schemas import UpdateProfileRequest
|
| 15 |
from src.profile.service import update_user_profile
|
| 16 |
+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from sqlmodel import select
|
|
|
|
|
|
|
| 18 |
from src.core.models import Users, Teams, Roles, UserTeamsRole
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/profile", tags=["Profile"])
|
| 22 |
|
| 23 |
+
# src/routers/gmail_oauth_router.py
|
| 24 |
+
from fastapi import APIRouter, Query, HTTPException
|
| 25 |
+
from fastapi.responses import RedirectResponse, JSONResponse
|
| 26 |
+
import httpx
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
router = APIRouter(prefix="/gmail", tags=["Gmail OAuth"])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@router.get("/login")
|
| 33 |
+
def google_login(state: str | None = Query(None)):
|
| 34 |
+
return RedirectResponse(build_auth_url(state))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get("/callback")
|
| 38 |
+
async def google_callback(code: str | None = None, state: str | None = None):
|
| 39 |
+
if not code:
|
| 40 |
+
raise HTTPException(400, "Missing code")
|
| 41 |
+
|
| 42 |
+
token_data = await exchange_code_for_tokens(code)
|
| 43 |
+
access_token = token_data["access_token"]
|
| 44 |
+
refresh_token = token_data.get("refresh_token")
|
| 45 |
+
|
| 46 |
+
# Get user info
|
| 47 |
+
async with httpx.AsyncClient() as client:
|
| 48 |
+
r = await client.get(
|
| 49 |
+
"https://www.googleapis.com/oauth2/v3/userinfo",
|
| 50 |
+
headers={"Authorization": f"Bearer {access_token}"}
|
| 51 |
+
)
|
| 52 |
+
userinfo = r.json()
|
| 53 |
+
|
| 54 |
+
google_user_id = userinfo["sub"]
|
| 55 |
+
user_email = userinfo["email"]
|
| 56 |
+
|
| 57 |
+
USER_TOKEN_STORE[google_user_id] = {
|
| 58 |
+
"access_token": access_token,
|
| 59 |
+
"refresh_token": refresh_token,
|
| 60 |
+
"email": user_email
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return JSONResponse({
|
| 64 |
+
"status": "ok",
|
| 65 |
+
"user_id": google_user_id,
|
| 66 |
+
"email": user_email,
|
| 67 |
+
"state": state,
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@router.post("/send-mail")
|
| 72 |
+
async def send_mail(req: SendMailRequest):
|
| 73 |
+
return await send_email_service(req)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
|
| 77 |
@router.get("/", response_model=BaseResponse)
|
| 78 |
async def get_assets(
|
|
|
|
| 106 |
return {"code": 200, "data": result}
|
| 107 |
|
| 108 |
|
|
|
|
| 109 |
@router.get("/contacts", response_model=BaseResponse)
|
| 110 |
async def get_leave_contacts(
|
| 111 |
current_user=Depends(get_current_user),
|
|
|
|
| 184 |
background.add_task(send_email, to_email, subject, body, cc, from_email)
|
| 185 |
|
| 186 |
return BaseResponse(code=200, message="Leave request sent", data=None)
|
|
|
src/profile/schemas.py
CHANGED
|
@@ -3,22 +3,26 @@ from typing import Optional
|
|
| 3 |
import uuid
|
| 4 |
from enum import Enum
|
| 5 |
|
|
|
|
| 6 |
class AssetStatus(str, Enum):
|
| 7 |
ACTIVE = "Active"
|
| 8 |
UNAVAILABLE = "Unavailable"
|
| 9 |
ON_REQUEST = "On Request"
|
| 10 |
IN_SERVICE = "In Service"
|
| 11 |
|
|
|
|
| 12 |
class AssetCreateRequest(BaseModel):
|
| 13 |
name: str
|
| 14 |
type: str
|
| 15 |
status: Optional[AssetStatus] = AssetStatus.UNAVAILABLE
|
| 16 |
|
|
|
|
| 17 |
class AssetUpdateRequest(BaseModel):
|
| 18 |
name: Optional[str] = None
|
| 19 |
type: Optional[str] = None
|
| 20 |
status: Optional[AssetStatus] = None
|
| 21 |
|
|
|
|
| 22 |
class AssetResponse(BaseModel):
|
| 23 |
id: uuid.UUID
|
| 24 |
user_id: uuid.UUID
|
|
@@ -26,6 +30,7 @@ class AssetResponse(BaseModel):
|
|
| 26 |
type: str
|
| 27 |
status: AssetStatus
|
| 28 |
|
|
|
|
| 29 |
class BaseResponse(BaseModel):
|
| 30 |
code: int
|
| 31 |
data: dict
|
|
@@ -39,3 +44,11 @@ class UpdateProfileRequest(BaseModel):
|
|
| 39 |
|
| 40 |
current_password: Optional[str] = None
|
| 41 |
new_password: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import uuid
|
| 4 |
from enum import Enum
|
| 5 |
|
| 6 |
+
|
| 7 |
class AssetStatus(str, Enum):
|
| 8 |
ACTIVE = "Active"
|
| 9 |
UNAVAILABLE = "Unavailable"
|
| 10 |
ON_REQUEST = "On Request"
|
| 11 |
IN_SERVICE = "In Service"
|
| 12 |
|
| 13 |
+
|
| 14 |
class AssetCreateRequest(BaseModel):
|
| 15 |
name: str
|
| 16 |
type: str
|
| 17 |
status: Optional[AssetStatus] = AssetStatus.UNAVAILABLE
|
| 18 |
|
| 19 |
+
|
| 20 |
class AssetUpdateRequest(BaseModel):
|
| 21 |
name: Optional[str] = None
|
| 22 |
type: Optional[str] = None
|
| 23 |
status: Optional[AssetStatus] = None
|
| 24 |
|
| 25 |
+
|
| 26 |
class AssetResponse(BaseModel):
|
| 27 |
id: uuid.UUID
|
| 28 |
user_id: uuid.UUID
|
|
|
|
| 30 |
type: str
|
| 31 |
status: AssetStatus
|
| 32 |
|
| 33 |
+
|
| 34 |
class BaseResponse(BaseModel):
|
| 35 |
code: int
|
| 36 |
data: dict
|
|
|
|
| 44 |
|
| 45 |
current_password: Optional[str] = None
|
| 46 |
new_password: Optional[str] = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class SendMailRequest(BaseModel):
|
| 50 |
+
user_id: str
|
| 51 |
+
to: EmailStr
|
| 52 |
+
subject: str
|
| 53 |
+
body: str
|
| 54 |
+
from_name: Optional[str] = None
|
src/profile/service.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from src.core.models import Assets
|
| 2 |
from ast import List
|
| 3 |
from datetime import datetime
|
|
@@ -10,10 +13,55 @@ from typing import List
|
|
| 10 |
from sqlmodel import select
|
| 11 |
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 12 |
from src.core.models import Assets
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
async def update_user_profile(session, user_id: str, data):
|
| 18 |
user = await session.get(Users, uuid.UUID(user_id))
|
| 19 |
|
|
@@ -71,8 +119,7 @@ async def update_user_profile(session, user_id: str, data):
|
|
| 71 |
},
|
| 72 |
}
|
| 73 |
|
|
|
|
| 74 |
async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
|
| 75 |
-
q = await session.exec(
|
| 76 |
-
|
| 77 |
-
)
|
| 78 |
-
return q.all()
|
|
|
|
| 1 |
+
from src.profile.utils import build_raw_message
|
| 2 |
+
from src.profile.utils import refresh_access_token
|
| 3 |
+
from src.profile.schemas import SendMailRequest
|
| 4 |
from src.core.models import Assets
|
| 5 |
from ast import List
|
| 6 |
from datetime import datetime
|
|
|
|
| 13 |
from sqlmodel import select
|
| 14 |
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 15 |
from src.core.models import Assets
|
| 16 |
+
import httpx
|
| 17 |
+
from src.core.config import settings
|
| 18 |
+
|
| 19 |
|
| 20 |
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 21 |
|
| 22 |
|
| 23 |
+
# In production, replace with DB storage
|
| 24 |
+
USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def send_email_service(req: SendMailRequest):
|
| 28 |
+
record = USER_TOKEN_STORE.get(req.user_id)
|
| 29 |
+
if not record:
|
| 30 |
+
raise HTTPException(404, "User not logged in with Google OAuth")
|
| 31 |
+
|
| 32 |
+
access_token = record["access_token"]
|
| 33 |
+
refresh_token = record.get("refresh_token")
|
| 34 |
+
|
| 35 |
+
if not access_token and refresh_token:
|
| 36 |
+
new_tokens = await refresh_access_token(refresh_token)
|
| 37 |
+
access_token = new_tokens["access_token"]
|
| 38 |
+
record["access_token"] = access_token
|
| 39 |
+
|
| 40 |
+
if not access_token:
|
| 41 |
+
raise HTTPException(400, "Re-auth required")
|
| 42 |
+
|
| 43 |
+
raw = build_raw_message(
|
| 44 |
+
to_email=req.to,
|
| 45 |
+
subject=req.subject,
|
| 46 |
+
body=req.body,
|
| 47 |
+
from_name=req.from_name,
|
| 48 |
+
from_email=record["email"],
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
|
| 52 |
+
payload = {"raw": raw}
|
| 53 |
+
|
| 54 |
+
async with httpx.AsyncClient() as client:
|
| 55 |
+
r = await client.post(
|
| 56 |
+
url, json=payload, headers={"Authorization": f"Bearer {access_token}"}
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if r.status_code >= 400:
|
| 60 |
+
raise HTTPException(500, f"Gmail error: {r.text}")
|
| 61 |
+
|
| 62 |
+
return r.json()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
async def update_user_profile(session, user_id: str, data):
|
| 66 |
user = await session.get(Users, uuid.UUID(user_id))
|
| 67 |
|
|
|
|
| 119 |
},
|
| 120 |
}
|
| 121 |
|
| 122 |
+
|
| 123 |
async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
|
| 124 |
+
q = await session.exec(select(Assets).where(Assets.user_id == uuid.UUID(user_id)))
|
| 125 |
+
return q.all()
|
|
|
|
|
|
src/profile/utils.py
CHANGED
|
@@ -3,6 +3,12 @@ import smtplib
|
|
| 3 |
from email.message import EmailMessage
|
| 4 |
from src.core.config import settings
|
| 5 |
from typing import List
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
SMTP_HOST = settings.EMAIL_SERVER
|
|
@@ -11,6 +17,69 @@ SMTP_USER = settings.EMAIL_USERNAME
|
|
| 11 |
SMTP_PASS = settings.EMAIL_PASSWORD
|
| 12 |
FROM_DEFAULT = settings.EMAIL_USERNAME
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
def send_email(
|
| 16 |
to_email: str, subject: str, body: str, cc: list[str] = None, from_email: str = None
|
|
|
|
| 3 |
from email.message import EmailMessage
|
| 4 |
from src.core.config import settings
|
| 5 |
from typing import List
|
| 6 |
+
import base64
|
| 7 |
+
import httpx
|
| 8 |
+
from typing import Dict, Optional
|
| 9 |
+
from src.core.config import settings
|
| 10 |
+
from urllib.parse import urlencode
|
| 11 |
+
|
| 12 |
|
| 13 |
|
| 14 |
SMTP_HOST = settings.EMAIL_SERVER
|
|
|
|
| 17 |
SMTP_PASS = settings.EMAIL_PASSWORD
|
| 18 |
FROM_DEFAULT = settings.EMAIL_USERNAME
|
| 19 |
|
| 20 |
+
# src/utils/gmail_utils.py
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def build_auth_url(state=None):
|
| 24 |
+
params = {
|
| 25 |
+
"client_id": settings.GOOGLE_CLIENT_ID,
|
| 26 |
+
"redirect_uri": settings.GOOGLE_REDIRECT_URI,
|
| 27 |
+
"response_type": "code",
|
| 28 |
+
"scope": settings.GMAIL_SEND_SCOPE + " openid email profile",
|
| 29 |
+
"access_type": "offline",
|
| 30 |
+
"prompt": "consent",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if state:
|
| 34 |
+
params["state"] = state
|
| 35 |
+
|
| 36 |
+
query = urlencode(params)
|
| 37 |
+
return f"{settings.AUTH_BASE}?{query}"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
async def exchange_code_for_tokens(code: str) -> Dict:
|
| 41 |
+
data = {
|
| 42 |
+
"code": code,
|
| 43 |
+
"client_id": settings.GOOGLE_CLIENT_ID,
|
| 44 |
+
"client_secret": settings.GOOGLE_CLIENT_SECRET,
|
| 45 |
+
"redirect_uri": settings.GOOGLE_REDIRECT_URI,
|
| 46 |
+
"grant_type": "authorization_code",
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
async with httpx.AsyncClient() as client:
|
| 50 |
+
r = await client.post(settings.TOKEN_URL, data=data)
|
| 51 |
+
r.raise_for_status()
|
| 52 |
+
return r.json()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
async def refresh_access_token(refresh_token: str) -> Dict:
|
| 56 |
+
data = {
|
| 57 |
+
"client_id": settings.GOOGLE_CLIENT_ID,
|
| 58 |
+
"client_secret": settings.GOOGLE_CLIENT_SECRET,
|
| 59 |
+
"refresh_token": refresh_token,
|
| 60 |
+
"grant_type": "refresh_token",
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
async with httpx.AsyncClient() as client:
|
| 64 |
+
r = await client.post(settings.TOKEN_URL, data=data)
|
| 65 |
+
r.raise_for_status()
|
| 66 |
+
return r.json()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_raw_message(
|
| 70 |
+
to_email: str, subject: str, body: str, from_name: Optional[str], from_email: str
|
| 71 |
+
) -> str:
|
| 72 |
+
|
| 73 |
+
msg = EmailMessage()
|
| 74 |
+
sender = f"{from_name} <{from_email}>" if from_name else from_email
|
| 75 |
+
msg["From"] = sender
|
| 76 |
+
msg["To"] = to_email
|
| 77 |
+
msg["Subject"] = subject
|
| 78 |
+
msg.set_content(body)
|
| 79 |
+
|
| 80 |
+
raw_bytes = msg.as_bytes()
|
| 81 |
+
return base64.urlsafe_b64encode(raw_bytes).decode()
|
| 82 |
+
|
| 83 |
|
| 84 |
def send_email(
|
| 85 |
to_email: str, subject: str, body: str, cc: list[str] = None, from_email: str = None
|