diff --git a/app/api/health.py b/app/api/health.py index 146a163..37356f4 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -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"} diff --git a/app/services/smtp.py b/app/services/smtp.py index d07cd03..dcb5b0f 100644 --- a/app/services/smtp.py +++ b/app/services/smtp.py @@ -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, @@ -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. @@ -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 \ No newline at end of file + raise