-
Notifications
You must be signed in to change notification settings - Fork 1
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
Email SeS implementation #19
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,38 @@ | ||
import json | ||
from abc import ABC, abstractmethod | ||
from dataclasses import dataclass | ||
from enum import Enum | ||
from typing import Generic, TypeVar | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
class TemplateData(ABC): | ||
def get_formatted_string(self) -> str: | ||
class_dict = self.__dict__ | ||
try: | ||
formatted_string = json.dumps(class_dict) # Try to convert to a JSON string | ||
except (TypeError, ValueError) as e: | ||
# Handle errors and return a message instead | ||
return f"Error in converting data to JSON: {e}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're planning on using this as the basis of other template data, I think we should propagate the error instead of gracefully failing, especially if this is being sent by email. |
||
|
||
return formatted_string | ||
|
||
|
||
@dataclass | ||
class TestEmailData(TemplateData): | ||
name: str | ||
date: str | ||
|
||
|
||
class EmailTemplate(Enum): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we name this |
||
TEST = "Test" | ||
|
||
|
||
@dataclass | ||
class EmailContent(Generic[T]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: let's call T Cool use of generics in this situation though! |
||
recipient: str | ||
data: T | ||
|
||
|
||
class IEmailService(ABC): | ||
|
@@ -8,7 +42,7 @@ class IEmailService(ABC): | |
""" | ||
|
||
@abstractmethod | ||
def send_email(self, to: str, subject: str, body: str) -> dict: | ||
def send_email(self, template: EmailTemplate, content: EmailContent) -> dict: | ||
""" | ||
Sends an email with the given parameters. | ||
|
||
|
@@ -23,50 +57,3 @@ def send_email(self, to: str, subject: str, body: str) -> dict: | |
:raises Exception: if email was not sent successfully | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def send_welcome_email(self, recipient: str, user_name: str) -> dict: | ||
""" | ||
Sends a welcome email to the specified user. | ||
|
||
:param recipient: Email address of the user | ||
:type recipient: str | ||
:param user_name: Name of the user | ||
:type user_name: str | ||
:return: Provider-specific metadata for the sent email | ||
:rtype: dict | ||
:raises Exception: if email was not sent successfully | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def send_password_reset_email(self, recipient: str, reset_link: str) -> dict: | ||
""" | ||
Sends a password reset email with the provided reset link. | ||
|
||
:param recipient: Email address of the user requesting the reset | ||
:type recipient: str | ||
:param reset_link: Password reset link | ||
:type reset_link: str | ||
:return: Provider-specific metadata for the sent email | ||
:rtype: dict | ||
:raises Exception: if email was not sent successfully | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def send_notification_email(self, recipient: str, notification_text: str) -> dict: | ||
""" | ||
Sends a notification email to the user with the provided notification text. | ||
Examples of use case include matches completed and ready to view, new messages, | ||
meeting time scheduled, etc. | ||
|
||
:param recipient: Email address of the user | ||
:type recipient: str | ||
:param notification_text: The notification content | ||
:type notification_text: str | ||
:return: Provider-specific metadata for the sent email | ||
:rtype: dict | ||
:raises Exception: if email was not sent successfully | ||
""" | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
from abc import ABC, abstractmethod | ||
|
||
from app.interfaces.email_service import EmailContent, EmailTemplate | ||
|
||
|
||
class IEmailServiceProvider(ABC): | ||
""" | ||
|
@@ -8,9 +10,7 @@ class IEmailServiceProvider(ABC): | |
""" | ||
|
||
@abstractmethod | ||
def send_email( | ||
self, recipient: str, subject: str, body_html: str, body_text: str | ||
) -> dict: | ||
def send_email(self, template: EmailTemplate, content: EmailContent) -> dict: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
""" | ||
Sends an email using the provider's service. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,34 @@ | ||
from app.interfaces.email_service import IEmailService | ||
import logging | ||
|
||
from app.interfaces.email_service import EmailContent, EmailTemplate, IEmailService, T | ||
from app.interfaces.email_service_provider import IEmailServiceProvider | ||
|
||
|
||
# TODO (Mayank, Nov 30th) - Implement the email service methods and use User object | ||
class EmailService(IEmailService): | ||
def __init__(self, provider: IEmailServiceProvider): | ||
self.provider = provider | ||
self.logger = logging.getLogger(__name__) | ||
|
||
# def render_templates(self, template: EmailTemplate) -> tuple[str, str]: | ||
# html_path = self.template_dir / f"{template.value}.html" | ||
# text_path = self.template_dir / f"{template.value}.txt" | ||
|
||
def send_email(self, to: str, subject: str, body: str) -> dict: | ||
pass | ||
# # Check if both files exist | ||
# if not html_path.exists(): | ||
# raise FileNotFoundError(f"HTML template not found: {html_path}") | ||
# if not text_path.exists(): | ||
# raise FileNotFoundError(f"Text template not found: {text_path}") | ||
|
||
def send_welcome_email(self, recipient: str, user_name: str) -> dict: | ||
pass | ||
# # Read the templates | ||
# html_template = html_path.read_text(encoding="utf-8") | ||
# text_template = text_path.read_text(encoding="utf-8") | ||
|
||
def send_password_reset_email(self, recipient: str, reset_link: str) -> dict: | ||
pass | ||
# return html_template, text_template | ||
|
||
def send_notification_email(self, recipient: str, notification_text: str) -> dict: | ||
pass | ||
def send_email(self, template: EmailTemplate, content: EmailContent[T]) -> dict: | ||
self.logger.info( | ||
f"Sending email to {content.recipient} with template {template.value}" | ||
) | ||
# html_template, text_template = self.render_templates(template) | ||
return self.provider.send_email(template, content) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,72 @@ | ||
import os | ||
|
||
import boto3 | ||
from botocore.exceptions import ClientError | ||
|
||
from app.interfaces.email_service import EmailContent, EmailTemplate | ||
from app.interfaces.email_service_provider import IEmailServiceProvider | ||
|
||
|
||
class AmazonSESEmailProvider(IEmailServiceProvider): | ||
def __init__(self, aws_access_key: str, aws_secret_key: str): | ||
pass | ||
|
||
# TODO (Mayank, Nov 30th) - Create an email object to pass into this method | ||
def send_email( | ||
self, recipient: str, subject: str, body_html: str, body_text: str | ||
) -> dict: | ||
pass | ||
def __init__( | ||
self, | ||
aws_access_key: str, | ||
aws_secret_key: str, | ||
region: str, | ||
source_email: str, | ||
is_sandbox: bool = True, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does |
||
): | ||
self.source_email = source_email | ||
self.is_sandbox = is_sandbox | ||
self.ses_client = boto3.client( | ||
"ses", | ||
region_name=region, | ||
aws_access_key_id=aws_access_key, | ||
aws_secret_access_key=aws_secret_key, | ||
) | ||
|
||
self.verified_emails = None | ||
if self.is_sandbox: | ||
response = self.ses_client.list_verified_email_addresses() | ||
self.verified_emails = response.get("VerifiedEmailAddresses", []) | ||
|
||
def _verify_email(self, email: str): | ||
if not self.is_sandbox: | ||
return | ||
try: | ||
if email not in self.verified_emails: | ||
self.ses_client.verify_email_identity(EmailAddress=email) | ||
print(f"Verification email sent to {email}.") | ||
except Exception as e: | ||
print(f"Failed to verify email: {e}") | ||
|
||
def send_email(self, template: EmailTemplate, content: EmailContent) -> dict: | ||
try: | ||
self._verify_email(content.recipient) | ||
|
||
self.ses_client.get_template(TemplateName=template.value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this line do right now? Should it be getting a response value? |
||
|
||
template_data = content.data.get_formatted_string() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Imo you should group this with |
||
|
||
response = self.ses_client.send_templated_email( | ||
Source=self.source_email, | ||
Destination={"ToAddresses": [content.recipient]}, | ||
Template=template.value, | ||
TemplateData=template_data, | ||
) | ||
|
||
return { | ||
"message": "Email sent successfully!", | ||
"message_id": response["MessageId"], | ||
} | ||
except ClientError as e: | ||
return {"error": f"An error occurred: {e.response['Error']['Message']}"} | ||
|
||
|
||
def get_email_service_provider() -> IEmailServiceProvider: | ||
return AmazonSESEmailProvider( | ||
aws_access_key=os.getenv("AWS_ACCESS_KEY"), | ||
aws_secret_key=os.getenv("AWS_SECRET_KEY"), | ||
region=os.getenv("AWS_REGION"), | ||
source_email=os.getenv("SES_SOURCE_EMAIL"), | ||
) |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to move this to a schema file? Given that
routes
is using this too, maybe we should treat this the same as a schema.