Skip to content

Commit 76a816d

Browse files
authored
Merge pull request #192 from grillazz/171-simple-and-fast-smtp-client
add endpoint to send email with smtp service
2 parents 30a1004 + 075a884 commit 76a816d

File tree

2 files changed

+103
-11
lines changed

2 files changed

+103
-11
lines changed

app/api/health.py

+91-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,102 @@
11
import logging
2+
from typing import Annotated
23

3-
from fastapi import APIRouter, status, Request
4+
from fastapi import APIRouter, status, Request, Depends, Query
5+
from pydantic import EmailStr
6+
from starlette.concurrency import run_in_threadpool
7+
8+
from app.services.smtp import SMTPEmailService
9+
10+
from app.utils.logging import AppLogger
11+
12+
logger = AppLogger().get_logger()
413

514
router = APIRouter()
615

716

817
@router.get("/redis", status_code=status.HTTP_200_OK)
918
async def redis_check(request: Request):
10-
_redis = await request.app.redis
11-
_info = None
19+
"""
20+
Endpoint to check Redis health and retrieve server information.
21+
22+
This endpoint connects to the Redis client configured in the application
23+
and attempts to fetch server information using the `info()` method.
24+
If an error occurs during the Redis operation, it logs the error.
25+
26+
Args:
27+
request (Request): The incoming HTTP request.
28+
29+
Returns:
30+
dict or None: Returns Redis server information as a dictionary if successful,
31+
otherwise returns `None` in case of an error.
32+
"""
33+
redis_client = await request.app.redis
34+
redis_info = None
1235
try:
13-
_info = await _redis.info()
36+
redis_info = await redis_client.info()
1437
except Exception as e:
1538
logging.error(f"Redis error: {e}")
16-
return _info
39+
return redis_info
40+
41+
42+
@router.post("/email", status_code=status.HTTP_200_OK)
43+
async def smtp_check(
44+
request: Request,
45+
smtp: Annotated[SMTPEmailService, Depends()],
46+
sender: Annotated[EmailStr, Query(description="Email address of the sender")],
47+
recipients: Annotated[
48+
list[EmailStr], Query(description="List of recipient email addresses")
49+
],
50+
subject: Annotated[str, Query(description="Subject line of the email")],
51+
body_text: Annotated[str, Query(description="Body text of the email")] = "",
52+
):
53+
"""
54+
Endpoint to send an email via an SMTP service.
55+
56+
This endpoint facilitates sending an email using the configured SMTP service. It performs
57+
the operation in a separate thread using `run_in_threadpool`, which is suitable for blocking I/O
58+
operations, such as sending emails. By offloading the sending process to a thread pool, it prevents
59+
the asynchronous event loop from being blocked, ensuring that other tasks in the application
60+
remain responsive.
61+
62+
Args:
63+
request (Request): The incoming HTTP request, providing context such as the base URL.
64+
smtp (SMTPEmailService): The SMTP email service dependency injected to send emails.
65+
sender (EmailStr): The sender's email address.
66+
recipients (list[EmailStr]): A list of recipient email addresses.
67+
subject (str): The subject line of the email.
68+
body_text (str, optional): The plain-text body of the email. Defaults to an empty string.
69+
70+
Returns:
71+
dict: A JSON object indicating success with a message, e.g., {"message": "Email sent"}.
72+
73+
Logs:
74+
Logs relevant email metadata: request base URL, sender, recipients, and subject.
75+
76+
Why `run_in_threadpool`:
77+
Sending an email often involves interacting with external SMTP servers, which can be
78+
a slow, blocking operation. Using `run_in_threadpool` is beneficial because:
79+
1. Blocking I/O operations like SMTP requests do not interrupt the main event loop,
80+
preventing other tasks (e.g., handling HTTP requests) from slowing down.
81+
2. The email-sending logic is offloaded to a separate, managed thread pool, improving
82+
application performance and scalability.
83+
"""
84+
85+
email_data = {
86+
"base_url": request.base_url,
87+
"sender": sender,
88+
"recipients": recipients,
89+
"subject": subject,
90+
}
91+
92+
logger.info("Sending email with data: %s", email_data)
93+
94+
await run_in_threadpool(
95+
smtp.send_email,
96+
sender=sender,
97+
recipients=recipients,
98+
subject=subject,
99+
body_text=body_text,
100+
body_html=None,
101+
)
102+
return {"message": "Email sent"}

app/services/smtp.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ def __attrs_post_init__(self):
5656
and logs in using the provided credentials.
5757
"""
5858
self.server = smtplib.SMTP(self.server_host, self.server_port)
59-
self.server.starttls() # Upgrade the connection to secure TLS
59+
self.server.starttls() # Upgrade the connection to secure TLS
6060
self.server.login(self.username, self.password)
61-
logger.info("SMTPEmailService initialized successfully and connected to SMTP server.")
61+
logger.info(
62+
"SMTPEmailService initialized successfully and connected to SMTP server."
63+
)
6264

6365
def _prepare_email(
6466
self,
@@ -141,7 +143,7 @@ def send_template_email(
141143
Args:
142144
recipients (list[EmailStr]): A list of recipient email addresses.
143145
subject (str): The subject line of the email.
144-
template (str): The name of the template file in the templates directory.
146+
template (str): The name of the template file in the templates' directory.
145147
context (dict): A dictionary of values to render the template with.
146148
sender (EmailStr): The email address of the sender.
147149
@@ -151,9 +153,13 @@ def send_template_email(
151153
"""
152154
try:
153155
template_str = self.templates.get_template(template)
154-
body_html = template_str.render(context) # Render the HTML using context variables
156+
body_html = template_str.render(
157+
context
158+
) # Render the HTML using context variables
155159
self.send_email(sender, recipients, subject, body_html=body_html)
156-
logger.info(f"Template email sent successfully to {recipients} using template {template}.")
160+
logger.info(
161+
f"Template email sent successfully to {recipients} using template {template}."
162+
)
157163
except Exception as e:
158164
logger.error("Failed to send template email", exc_info=e)
159-
raise
165+
raise

0 commit comments

Comments
 (0)