From 32d5b8936038a777f0e73ac3767fa531d080ab1a Mon Sep 17 00:00:00 2001 From: Tauquir <30658453+itstauq@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:22:08 +0400 Subject: [PATCH] Add API endpoints for sending emails --- syftbox/server/emails/constants.py | 2 + syftbox/server/emails/models.py | 24 ++++++++++++ syftbox/server/emails/router.py | 60 ++++++++++++++++++++++++++++++ syftbox/server/server.py | 2 + syftbox/server/settings.py | 3 ++ 5 files changed, 91 insertions(+) create mode 100644 syftbox/server/emails/constants.py create mode 100644 syftbox/server/emails/models.py create mode 100644 syftbox/server/emails/router.py diff --git a/syftbox/server/emails/constants.py b/syftbox/server/emails/constants.py new file mode 100644 index 00000000..64f22463 --- /dev/null +++ b/syftbox/server/emails/constants.py @@ -0,0 +1,2 @@ +EMAIL_SERVICE_API_URL = "https://api.resend.com/emails" +FROM_EMAIl = "OpenMined SyftBox " diff --git a/syftbox/server/emails/models.py b/syftbox/server/emails/models.py new file mode 100644 index 00000000..7d998955 --- /dev/null +++ b/syftbox/server/emails/models.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, EmailStr, Field, NameEmail + +from .constants import FROM_EMAIl + + +class SendEmailRequest(BaseModel): + to: EmailStr | NameEmail + subject: str + html: str + + def json_for_request(self): + return { + "from": FROM_EMAIl, + "to": [self.to], + "subject": self.subject, + "html": self.html, + } + + +class BatchSendEmailRequest(BaseModel): + emails: list[SendEmailRequest] = Field(max_length=100) + + def json_for_request(self): + return [email.json_for_request() for email in self.emails] diff --git a/syftbox/server/emails/router.py b/syftbox/server/emails/router.py new file mode 100644 index 00000000..6bc21d17 --- /dev/null +++ b/syftbox/server/emails/router.py @@ -0,0 +1,60 @@ +import httpx +from fastapi import APIRouter, Depends +from loguru import logger + +from syftbox.server.emails.models import BatchSendEmailRequest, SendEmailRequest +from syftbox.server.settings import ServerSettings, get_server_settings + +from .constants import EMAIL_SERVICE_API_URL + +router = APIRouter(prefix="/emails", tags=["email"]) + + +@router.post("/") +async def send_email( + email_request: SendEmailRequest, + server_settings: ServerSettings = Depends(get_server_settings), +) -> bool: + async with httpx.AsyncClient() as client: + response = await client.post( + EMAIL_SERVICE_API_URL, + headers={ + "Authorization": f"Bearer {server_settings.email_service_api_key}", + "Content-Type": "application/json", + }, + json=email_request.json_for_request(), + ) + if response.status_code == 200: + sent_to = email_request.to if isinstance(email_request.to, str) else ", ".join(email_request.to) + logger.info(f"Email sent successfully to {sent_to}") + return True + else: + logger.error(f"Failed to send email: {response.text}") + return False + + +@router.post("/batch") +async def send_batch_email( + email_requests: BatchSendEmailRequest, + server_settings: ServerSettings = Depends(get_server_settings), +) -> bool: + """Trigger up to 100 batch emails at once. + + Instead of sending one email per HTTP request, we provide a batching endpoint that + permits you to send up to 100 emails in a single API call. + """ + async with httpx.AsyncClient() as client: + response = await client.post( + f"{EMAIL_SERVICE_API_URL}/batch", + headers={ + "Authorization": f"Bearer {server_settings.email_service_api_key}", + "Content-Type": "application/json", + }, + json=email_requests.json_for_request(), + ) + if response.status_code == 200: + logger.info(f"{len(email_requests)} emails sent successfully") + return True + else: + logger.error(f"Failed to send email: {response.text}") + return False diff --git a/syftbox/server/server.py b/syftbox/server/server.py index bc8f3743..b22f4318 100644 --- a/syftbox/server/server.py +++ b/syftbox/server/server.py @@ -31,6 +31,7 @@ from syftbox.server.middleware import LoguruMiddleware from syftbox.server.settings import ServerSettings, get_server_settings +from .emails.router import router as emails_router from .sync import db, hash from .sync.router import router as sync_router @@ -163,6 +164,7 @@ async def lifespan(app: FastAPI, settings: Optional[ServerSettings] = None): app = FastAPI(lifespan=lifespan) +app.include_router(emails_router) app.include_router(sync_router) app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5) app.add_middleware(LoguruMiddleware) diff --git a/syftbox/server/settings.py b/syftbox/server/settings.py index 1e35d125..d08d1738 100644 --- a/syftbox/server/settings.py +++ b/syftbox/server/settings.py @@ -22,6 +22,9 @@ class ServerSettings(BaseSettings): data_folder: Path = Field(default=Path("data").resolve()) """Absolute path to the server data folder""" + email_service_api_key: str + """API key for the email service""" + @field_validator("data_folder", mode="after") def data_folder_abs(cls, v): return Path(v).expanduser().resolve()