Skip to content

Commit c2a6548

Browse files
committed
basic email sending
1 parent b74cc47 commit c2a6548

File tree

8 files changed

+168
-80
lines changed

8 files changed

+168
-80
lines changed

backend/app/interfaces/email_service.py

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class IEmailService(ABC):
88
"""
99

1010
@abstractmethod
11-
def send_email(self, to: str, subject: str, body: str) -> dict:
11+
def send_email(self, subject: str, recipient: str, body_html: str) -> None:
1212
"""
1313
Sends an email with the given parameters.
1414
@@ -23,50 +23,3 @@ def send_email(self, to: str, subject: str, body: str) -> dict:
2323
:raises Exception: if email was not sent successfully
2424
"""
2525
pass
26-
27-
@abstractmethod
28-
def send_welcome_email(self, recipient: str, user_name: str) -> dict:
29-
"""
30-
Sends a welcome email to the specified user.
31-
32-
:param recipient: Email address of the user
33-
:type recipient: str
34-
:param user_name: Name of the user
35-
:type user_name: str
36-
:return: Provider-specific metadata for the sent email
37-
:rtype: dict
38-
:raises Exception: if email was not sent successfully
39-
"""
40-
pass
41-
42-
@abstractmethod
43-
def send_password_reset_email(self, recipient: str, reset_link: str) -> dict:
44-
"""
45-
Sends a password reset email with the provided reset link.
46-
47-
:param recipient: Email address of the user requesting the reset
48-
:type recipient: str
49-
:param reset_link: Password reset link
50-
:type reset_link: str
51-
:return: Provider-specific metadata for the sent email
52-
:rtype: dict
53-
:raises Exception: if email was not sent successfully
54-
"""
55-
pass
56-
57-
@abstractmethod
58-
def send_notification_email(self, recipient: str, notification_text: str) -> dict:
59-
"""
60-
Sends a notification email to the user with the provided notification text.
61-
Examples of use case include matches completed and ready to view, new messages,
62-
meeting time scheduled, etc.
63-
64-
:param recipient: Email address of the user
65-
:type recipient: str
66-
:param notification_text: The notification content
67-
:type notification_text: str
68-
:return: Provider-specific metadata for the sent email
69-
:rtype: dict
70-
:raises Exception: if email was not sent successfully
71-
"""
72-
pass

backend/app/interfaces/email_service_provider.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ class IEmailServiceProvider(ABC):
88
"""
99

1010
@abstractmethod
11-
def send_email(
12-
self, recipient: str, subject: str, body_html: str, body_text: str
13-
) -> dict:
11+
def send_email(self, recipient: str, subject: str) -> None:
1412
"""
1513
Sends an email using the provider's service.
1614

backend/app/routes/email.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
1+
import logging
12
from typing import Annotated
23

34
from fastapi import APIRouter, Depends
45

56
from app.interfaces.email_service import IEmailService
67
from app.services.email.email_service import EmailService
7-
from app.services.email.email_service_provider import AmazonSESEmailProvider
8+
from app.services.email.email_service_provider import (
9+
get_email_service_provider,
10+
)
811

912
router = APIRouter(
1013
prefix="/email",
1114
tags=["email"],
1215
)
1316

17+
log = logging.getLogger("uvicorn")
18+
1419

1520
def get_email_service() -> IEmailService:
16-
email_provider = AmazonSESEmailProvider(aws_access_key="", aws_secret_key="")
17-
return EmailService(email_provider)
21+
return EmailService(get_email_service_provider())
1822

1923

2024
# TODO (Mayank, Nov 30th) - Remove test emails once email service is fully implemented
21-
@router.post("/send-test-email/")
25+
@router.post("/send-test")
2226
async def send_welcome_email(
2327
recipient: str,
2428
user_name: str,
2529
email_service: Annotated[IEmailService, Depends(get_email_service)],
2630
):
27-
email_service.send_welcome_email(recipient, user_name)
31+
log.info(f"Main Sending welcome email to {user_name} at {recipient}")
32+
email_service.send_email(
33+
subject="Welcome to the app!",
34+
recipient=recipient,
35+
)
2836
return {"message": f"Welcome email sent to {user_name}!"}

backend/app/server.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,16 @@
1010
load_dotenv()
1111

1212
# we need to load env variables before initialization code runs
13-
from . import models # noqa: E402
1413
from .routes import user # noqa: E402
15-
from .utilities.firebase_init import initialize_firebase # noqa: E402
1614

1715
log = logging.getLogger("uvicorn")
1816

1917

2018
@asynccontextmanager
2119
async def lifespan(_: FastAPI):
2220
log.info("Starting up...")
23-
models.run_migrations()
24-
initialize_firebase()
21+
# models.run_migrations()
22+
# initialize_firebase()
2523
yield
2624
log.info("Shutting down...")
2725

@@ -36,6 +34,7 @@ async def lifespan(_: FastAPI):
3634

3735
@app.get("/")
3836
def read_root():
37+
log.info("Hello World")
3938
return {"Hello": "World"}
4039

4140

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from app.interfaces.email_service import IEmailService
24
from app.interfaces.email_service_provider import IEmailServiceProvider
35

@@ -6,15 +8,8 @@
68
class EmailService(IEmailService):
79
def __init__(self, provider: IEmailServiceProvider):
810
self.provider = provider
11+
self.logger = logging.getLogger(__name__)
912

10-
def send_email(self, to: str, subject: str, body: str) -> dict:
11-
pass
12-
13-
def send_welcome_email(self, recipient: str, user_name: str) -> dict:
14-
pass
15-
16-
def send_password_reset_email(self, recipient: str, reset_link: str) -> dict:
17-
pass
18-
19-
def send_notification_email(self, recipient: str, notification_text: str) -> dict:
20-
pass
13+
def send_email(self, subject: str, recipient: str, body_html: str = "") -> None:
14+
self.logger.info(f"Sending email to {recipient} with subject: {subject}")
15+
self.provider.send_email(subject, recipient)
Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,63 @@
1+
import os
2+
3+
import boto3
4+
from botocore.exceptions import BotoCoreError, ClientError
5+
from fastapi import HTTPException
6+
17
from app.interfaces.email_service_provider import IEmailServiceProvider
28

39

410
class AmazonSESEmailProvider(IEmailServiceProvider):
5-
def __init__(self, aws_access_key: str, aws_secret_key: str):
6-
pass
7-
8-
# TODO (Mayank, Nov 30th) - Create an email object to pass into this method
9-
def send_email(
10-
self, recipient: str, subject: str, body_html: str, body_text: str
11-
) -> dict:
12-
pass
11+
def __init__(
12+
self,
13+
aws_access_key: str,
14+
aws_secret_key: str,
15+
region: str,
16+
source_email: str,
17+
is_sandbox: bool = True,
18+
):
19+
self.source_email = source_email
20+
self.is_sandbox = is_sandbox
21+
self.ses_client = boto3.client(
22+
"ses",
23+
region_name=region,
24+
aws_access_key_id=aws_access_key,
25+
aws_secret_access_key=aws_secret_key,
26+
)
27+
28+
def _verify_email(self, email: str):
29+
if not self.is_sandbox:
30+
return
31+
try:
32+
self.client.verify_email_identity(EmailAddress=email)
33+
print(f"Verification email sent to {email}.")
34+
except Exception as e:
35+
print(f"Failed to verify email: {e}")
36+
37+
def send_email(self, subject: str, recipient: str) -> None:
38+
try:
39+
self._verify_email(recipient)
40+
self.ses_client.send_email(
41+
Source=self.source_email,
42+
Destination={"ToAddresses": [recipient]},
43+
Message={
44+
"Subject": {"Data": subject},
45+
"Body": {"Text": {"Data": "Hello, this is a test email!"}},
46+
},
47+
)
48+
except BotoCoreError as e:
49+
raise HTTPException(status_code=500, detail=f"SES BotoCoreError: {e}")
50+
except ClientError as e:
51+
raise HTTPException(
52+
status_code=500,
53+
detail=f"SES ClientError: {e.response['Error']['Message']}",
54+
)
55+
56+
57+
def get_email_service_provider() -> IEmailServiceProvider:
58+
return AmazonSESEmailProvider(
59+
aws_access_key=os.getenv("AWS_ACCESS_KEY"),
60+
aws_secret_key=os.getenv("AWS_SECRET_KEY"),
61+
region=os.getenv("AWS_REGION"),
62+
source_email=os.getenv("SES_SOURCE_EMAIL"),
63+
)

backend/pdm.lock

Lines changed: 84 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies = [
1717
"inflection>=0.5.1",
1818
"pre-commit>=4.0.0",
1919
"psycopg2>=2.9.9",
20+
"boto3>=1.35.71",
2021
]
2122
requires-python = "==3.12.*"
2223
readme = "README.md"

0 commit comments

Comments
 (0)