Skip to content

Commit

Permalink
Merge pull request #405 from OpenMined/tauquir/email-apis
Browse files Browse the repository at this point in the history
Add server API endpoints for sending emails
  • Loading branch information
itstauq authored Nov 18, 2024
2 parents 78a223f + 98b02c7 commit 0f5ebe7
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 0 deletions.
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

0 comments on commit 0f5ebe7

Please sign in to comment.