Skip to content

Commit

Permalink
Merge pull request #192 from grillazz/171-simple-and-fast-smtp-client
Browse files Browse the repository at this point in the history
add endpoint to send email with smtp service
  • Loading branch information
grillazz authored Dec 30, 2024
2 parents 30a1004 + 075a884 commit 76a816d
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 11 deletions.
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

0 comments on commit 76a816d

Please sign in to comment.