Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server API endpoints for sending emails #405

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions syftbox/server/emails/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EMAIL_SERVICE_API_URL = "https://api.resend.com/emails"
FROM_EMAIl = "OpenMined SyftBox <noreply@syftbox.openmined.org>"
26 changes: 26 additions & 0 deletions syftbox/server/emails/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Union

from pydantic import BaseModel, EmailStr, Field, NameEmail

from .constants import FROM_EMAIl


class SendEmailRequest(BaseModel):
to: Union[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]
66 changes: 66 additions & 0 deletions syftbox/server/emails/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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"])

# TODO add some safety mechanisms to the below endpoints (rate limiting, authorization, etc)


@router.post("/")
async def send_email(
email_request: SendEmailRequest,
server_settings: ServerSettings = Depends(get_server_settings),
) -> bool:
if not server_settings.email_service_api_key:
raise httpx.HTTPStatusError("Email service API key is not set", request=None, response=None)
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.
"""
if not server_settings.email_service_api_key:
raise httpx.HTTPStatusError("Email service API key is not set", request=None, response=None)
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
2 changes: 2 additions & 0 deletions syftbox/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions syftbox/server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = Field(default="")
"""API key for the email service"""

@field_validator("data_folder", mode="after")
def data_folder_abs(cls, v):
return Path(v).expanduser().resolve()
Expand Down