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 endpoint to send email with smtp service #192

Merged
merged 1 commit into from
Dec 30, 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
96 changes: 91 additions & 5 deletions app/api/health.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,102 @@
import logging
from typing import Annotated

from fastapi import APIRouter, status, Request
from fastapi import APIRouter, status, Request, Depends, Query
from pydantic import EmailStr
from starlette.concurrency import run_in_threadpool

from app.services.smtp import SMTPEmailService

from app.utils.logging import AppLogger

logger = AppLogger().get_logger()

router = APIRouter()


@router.get("/redis", status_code=status.HTTP_200_OK)
async def redis_check(request: Request):
_redis = await request.app.redis
_info = None
"""
Endpoint to check Redis health and retrieve server information.

This endpoint connects to the Redis client configured in the application
and attempts to fetch server information using the `info()` method.
If an error occurs during the Redis operation, it logs the error.

Args:
request (Request): The incoming HTTP request.

Returns:
dict or None: Returns Redis server information as a dictionary if successful,
otherwise returns `None` in case of an error.
"""
redis_client = await request.app.redis
redis_info = None
try:
_info = await _redis.info()
redis_info = await redis_client.info()
except Exception as e:
logging.error(f"Redis error: {e}")
return _info
return redis_info


@router.post("/email", status_code=status.HTTP_200_OK)
async def smtp_check(
request: Request,
smtp: Annotated[SMTPEmailService, Depends()],
sender: Annotated[EmailStr, Query(description="Email address of the sender")],
recipients: Annotated[
list[EmailStr], Query(description="List of recipient email addresses")
],
subject: Annotated[str, Query(description="Subject line of the email")],
body_text: Annotated[str, Query(description="Body text of the email")] = "",
):
"""
Endpoint to send an email via an SMTP service.

This endpoint facilitates sending an email using the configured SMTP service. It performs
the operation in a separate thread using `run_in_threadpool`, which is suitable for blocking I/O
operations, such as sending emails. By offloading the sending process to a thread pool, it prevents
the asynchronous event loop from being blocked, ensuring that other tasks in the application
remain responsive.

Args:
request (Request): The incoming HTTP request, providing context such as the base URL.
smtp (SMTPEmailService): The SMTP email service dependency injected to send emails.
sender (EmailStr): The sender's email address.
recipients (list[EmailStr]): A list of recipient email addresses.
subject (str): The subject line of the email.
body_text (str, optional): The plain-text body of the email. Defaults to an empty string.

Returns:
dict: A JSON object indicating success with a message, e.g., {"message": "Email sent"}.

Logs:
Logs relevant email metadata: request base URL, sender, recipients, and subject.

Why `run_in_threadpool`:
Sending an email often involves interacting with external SMTP servers, which can be
a slow, blocking operation. Using `run_in_threadpool` is beneficial because:
1. Blocking I/O operations like SMTP requests do not interrupt the main event loop,
preventing other tasks (e.g., handling HTTP requests) from slowing down.
2. The email-sending logic is offloaded to a separate, managed thread pool, improving
application performance and scalability.
"""

email_data = {
"base_url": request.base_url,
"sender": sender,
"recipients": recipients,
"subject": subject,
}

logger.info("Sending email with data: %s", email_data)

await run_in_threadpool(
smtp.send_email,
sender=sender,
recipients=recipients,
subject=subject,
body_text=body_text,
body_html=None,
)
return {"message": "Email sent"}
18 changes: 12 additions & 6 deletions app/services/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ def __attrs_post_init__(self):
and logs in using the provided credentials.
"""
self.server = smtplib.SMTP(self.server_host, self.server_port)
self.server.starttls() # Upgrade the connection to secure TLS
self.server.starttls() # Upgrade the connection to secure TLS
self.server.login(self.username, self.password)
logger.info("SMTPEmailService initialized successfully and connected to SMTP server.")
logger.info(
"SMTPEmailService initialized successfully and connected to SMTP server."
)

def _prepare_email(
self,
Expand Down Expand Up @@ -141,7 +143,7 @@ def send_template_email(
Args:
recipients (list[EmailStr]): A list of recipient email addresses.
subject (str): The subject line of the email.
template (str): The name of the template file in the templates directory.
template (str): The name of the template file in the templates' directory.
context (dict): A dictionary of values to render the template with.
sender (EmailStr): The email address of the sender.

Expand All @@ -151,9 +153,13 @@ def send_template_email(
"""
try:
template_str = self.templates.get_template(template)
body_html = template_str.render(context) # Render the HTML using context variables
body_html = template_str.render(
context
) # Render the HTML using context variables
self.send_email(sender, recipients, subject, body_html=body_html)
logger.info(f"Template email sent successfully to {recipients} using template {template}.")
logger.info(
f"Template email sent successfully to {recipients} using template {template}."
)
except Exception as e:
logger.error("Failed to send template email", exc_info=e)
raise
raise
Loading