From 7e2449f4d5d1e1b1d40ebb03ad2caae0ece1205d Mon Sep 17 00:00:00 2001
From: Benjamin Ng <60500272+benymng@users.noreply.github.com>
Date: Sun, 14 Apr 2024 18:47:06 -0400
Subject: [PATCH] Better Email service and regular reminder email functionality
---
backend/app/__init__.py | 23 ++-
backend/app/graphql/__init__.py | 55 +++++---
backend/app/graphql/services.py | 4 +
.../services/implementations/auth_service.py | 52 ++-----
.../services/implementations/email_service.py | 6 +
.../implementations/meal_request_service.py | 65 ++++++++-
.../implementations/mock_email_service.py | 3 +
.../implementations/reminder_email_service.py | 118 ++++++++++++++++
.../interfaces/meal_request_service.py | 18 +++
.../interfaces/reminder_email_service.py | 11 ++
.../committed_to_meal_request.html | 11 ++
.../donor_one_day_after_meal.html | 5 +
.../donor_one_day_to_meal.html | 13 ++
.../email_templates/meal_request_success.html | 11 ++
.../onboarding_request_approved.html | 5 +
.../onboarding_request_rejected.html | 3 +
.../requestor_one_day_after_meal.html | 3 +
.../requestor_one_day_to_meal.html | 11 ++
backend/email_templates/reset_password.html | 5 +
.../email_templates/verification_email.html | 9 ++
backend/requirements.txt | 3 +
backend/tests/graphql/conftest.py | 11 ++
.../tests/graphql/test_all_user_mutations.py | 39 ++++++
backend/tests/graphql/test_meal_request.py | 46 +++++-
.../tests/graphql/test_onboarding_request.py | 12 +-
.../graphql/test_reminder_email_service.py | 132 ++++++++++++++++++
26 files changed, 601 insertions(+), 73 deletions(-)
create mode 100644 backend/app/services/implementations/reminder_email_service.py
create mode 100644 backend/app/services/interfaces/reminder_email_service.py
create mode 100644 backend/email_templates/committed_to_meal_request.html
create mode 100644 backend/email_templates/donor_one_day_after_meal.html
create mode 100644 backend/email_templates/donor_one_day_to_meal.html
create mode 100644 backend/email_templates/meal_request_success.html
create mode 100644 backend/email_templates/onboarding_request_approved.html
create mode 100644 backend/email_templates/onboarding_request_rejected.html
create mode 100644 backend/email_templates/requestor_one_day_after_meal.html
create mode 100644 backend/email_templates/requestor_one_day_to_meal.html
create mode 100644 backend/email_templates/reset_password.html
create mode 100644 backend/email_templates/verification_email.html
create mode 100644 backend/tests/graphql/test_reminder_email_service.py
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
index f9f384cd..6ab23529 100644
--- a/backend/app/__init__.py
+++ b/backend/app/__init__.py
@@ -12,8 +12,10 @@
from .graphql import schema as graphql_schema
+from flask_apscheduler import APScheduler
+
+
def create_app(config_name):
- # configure Flask logger
dictConfig(
{
"version": 1,
@@ -56,6 +58,7 @@ def create_app(config_name):
re.compile(r"^https:\/\/uw-blueprint-starter-code--pr.*\.web\.app$"),
]
app.config["CORS_SUPPORTS_CREDENTIALS"] = True
+ app.config["SCHEDULER_API_ENABLED"] = True
CORS(app)
firebase_admin.initialize_app(
@@ -85,6 +88,22 @@ def create_app(config_name):
from . import models, graphql
models.init_app(app)
- graphql.init_app(app)
+ services = graphql.init_app(app)
+
+ scheduler = APScheduler()
+ scheduler.init_app(app)
+
+ # checks every hour for meal requests that were either yesterday or today and sends an email to the donor and requestor
+ @scheduler.task(
+ "interval", id="daily_job", seconds=60 * 60 * 24, misfire_grace_time=900
+ )
+ def dailyJob():
+ try:
+ with scheduler.app.app_context():
+ services["reminder_email_service"].send_regularly_scheduled_emails()
+ except Exception as e:
+ print("Error in Scheduled Task!", e)
+
+ scheduler.start()
return app
diff --git a/backend/app/graphql/__init__.py b/backend/app/graphql/__init__.py
index b79bc272..9f784a59 100644
--- a/backend/app/graphql/__init__.py
+++ b/backend/app/graphql/__init__.py
@@ -1,5 +1,6 @@
import graphene
import os
+from app.services.implementations.reminder_email_service import ReminderEmailService
from flask import current_app
@@ -53,28 +54,33 @@ class RootMutation(
)
+def init_email_service(app):
+ print("Initializing email service")
+ if app.config["TESTING"]:
+ print("Using mock email service in testings!")
+ services["email_service"] = MockEmailService(
+ logger=current_app.logger,
+ credentials={},
+ sender_email=os.getenv("MAILER_USER"),
+ display_name="Feeding Canadian Kids",
+ )
+ else:
+ services["email_service"] = EmailService(
+ logger=current_app.logger,
+ credentials={
+ "refresh_token": os.getenv("MAILER_REFRESH_TOKEN"),
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "client_id": os.getenv("MAILER_CLIENT_ID"),
+ "client_secret": os.getenv("MAILER_CLIENT_SECRET"),
+ },
+ sender_email=os.getenv("MAILER_USER"),
+ display_name="Feeding Canadian Kids",
+ )
+
+
def init_app(app):
with app.app_context():
- if app.config["TESTING"]:
- print("Using mock email service in testings!")
- services["email_service"] = MockEmailService(
- logger=current_app.logger,
- credentials={},
- sender_email=os.getenv("MAILER_USER"),
- display_name="Feeding Canadian Kids",
- )
- else:
- services["email_service"] = EmailService(
- logger=current_app.logger,
- credentials={
- "refresh_token": os.getenv("MAILER_REFRESH_TOKEN"),
- "token_uri": "https://oauth2.googleapis.com/token",
- "client_id": os.getenv("MAILER_CLIENT_ID"),
- "client_secret": os.getenv("MAILER_CLIENT_SECRET"),
- },
- sender_email=os.getenv("MAILER_USER"),
- display_name="Feeding Canadian Kids",
- )
+ init_email_service(app)
services["onsite_contact_service"] = OnsiteContactService(
logger=current_app.logger
)
@@ -90,4 +96,11 @@ def init_app(app):
services["onboarding_request_service"] = OnboardingRequestService(
logger=current_app.logger, email_service=services["email_service"]
)
- services["meal_request_service"] = MealRequestService(logger=current_app.logger)
+ services["meal_request_service"] = MealRequestService(
+ logger=current_app.logger, email_service=services["email_service"]
+ )
+ services["reminder_email_service"] = ReminderEmailService(
+ logger=current_app.logger, email_service=services["email_service"]
+ )
+
+ return services
diff --git a/backend/app/graphql/services.py b/backend/app/graphql/services.py
index afe665db..63299b63 100644
--- a/backend/app/graphql/services.py
+++ b/backend/app/graphql/services.py
@@ -1,11 +1,13 @@
from typing import TypedDict
+
from ..services.interfaces.onsite_contact_service import IOnsiteContactService
from ..services.interfaces.user_service import IUserService
from ..services.interfaces.auth_service import IAuthService
from ..services.interfaces.email_service import IEmailService
from ..services.interfaces.onboarding_request_service import IOnboardingRequestService
from ..services.interfaces.meal_request_service import IMealRequestService
+from ..services.interfaces.reminder_email_service import IReminderEmailService
"""
Global services for GraphQL that will be initialized with
@@ -20,6 +22,7 @@ class ServicesObject(TypedDict):
onboarding_request_service: IOnboardingRequestService
meal_request_service: IMealRequestService
onsite_contact_service: IOnsiteContactService
+ reminder_email_service: IReminderEmailService
services: ServicesObject = {
@@ -29,4 +32,5 @@ class ServicesObject(TypedDict):
"onboarding_request_service": None,
"meal_request_service": None,
"onsite_contact_service": None,
+ "reminder_email_service": None,
} # type: ignore
diff --git a/backend/app/services/implementations/auth_service.py b/backend/app/services/implementations/auth_service.py
index 48f0dcb4..530b664d 100644
--- a/backend/app/services/implementations/auth_service.py
+++ b/backend/app/services/implementations/auth_service.py
@@ -1,3 +1,4 @@
+from app.services.implementations.email_service import EmailService
import firebase_admin.auth
from ..interfaces.auth_service import IAuthService
@@ -115,18 +116,9 @@ def forgot_password(self, email):
set_password_link = "{url}/{ObjectID}/reset-password".format(
url=url, ObjectID=user.id
)
-
- email_body = """
- Hello,
-
- We have received your reset password request.
- Please reset your password using the following link.
-
- Reset Password
- """.format(
- reset_link=set_password_link
- )
-
+ email_body = EmailService.read_email_template(
+ "email_templates/reset_password.html"
+ ).format(reset_link=set_password_link)
self.email_service.send_email(email, "FCK Reset Password Link", email_body)
except Exception as e:
@@ -175,17 +167,9 @@ def send_email_verification_link(self, email):
verification_link = firebase_admin.auth.generate_email_verification_link(
email
)
- email_body = """
- Hello,
-
- Please click the following link to verify your email and
- activate your account.
- This link is only valid for 1 hour.
-
- Verify email
- """.format(
- verification_link=verification_link
- )
+ email_body = EmailService.read_email_template(
+ "email_templates/verification_email.html"
+ ).format(verification_link=verification_link)
self.email_service.send_email(email, "Verify your email", email_body)
except Exception as e:
self.logger.error(
@@ -209,16 +193,9 @@ def send_onboarding_request_approve_email(self, objectID, email):
url=url, ObjectID=objectID
)
- email_body = """
- Hello,
-
- We have received your onboarding request and it has been approved.
- Please set your password using the following link.
-
- Reset Password
- """.format(
- reset_link=set_password_link
- )
+ email_body = EmailService.read_email_template(
+ "email_templates/onboarding_request_approved.html"
+ ).format(set_password_link=set_password_link)
self.email_service.send_email(
email, "Onboarding request approved. Set Password", email_body
@@ -240,12 +217,9 @@ def send_onboarding_request_rejected_email(self, email):
raise Exception(error_message)
try:
- email_body = """
- Hello,
-
- This is a notification that your onboarding request has been rejected.
-
- """
+ email_body = EmailService.EmailService.read_email_template(
+ "email_templates/onboarding_request_rejected.html"
+ )
self.email_service.send_email(
email, "Onboarding request rejected", email_body
)
diff --git a/backend/app/services/implementations/email_service.py b/backend/app/services/implementations/email_service.py
index 4881d091..ca8822fc 100644
--- a/backend/app/services/implementations/email_service.py
+++ b/backend/app/services/implementations/email_service.py
@@ -10,6 +10,11 @@ class EmailService(IEmailService):
EmailService implementation for handling email related functionality
"""
+ @staticmethod
+ def read_email_template(file_path: str):
+ with open(file_path, "r") as file:
+ return file.read()
+
def __init__(self, logger, credentials, sender_email, display_name=None):
"""
Create an instance of EmailService
@@ -24,6 +29,7 @@ def __init__(self, logger, credentials, sender_email, display_name=None):
:param display_name: the sender's display name, defaults to None
:type display_name: str, optional
"""
+
self.logger = logger
creds = Credentials(None, **credentials)
self.service = build("gmail", "v1", credentials=creds)
diff --git a/backend/app/services/implementations/meal_request_service.py b/backend/app/services/implementations/meal_request_service.py
index 00478308..52d84e18 100644
--- a/backend/app/services/implementations/meal_request_service.py
+++ b/backend/app/services/implementations/meal_request_service.py
@@ -1,4 +1,6 @@
from typing import List
+from app.services.interfaces.email_service import IEmailService
+from app.services.implementations.email_service import EmailService
from ...models.meal_request import MealInfo, MealRequest
from ..interfaces.meal_request_service import IMealRequestService
from datetime import datetime
@@ -11,8 +13,9 @@
class MealRequestService(IMealRequestService):
- def __init__(self, logger):
+ def __init__(self, logger, email_service: IEmailService):
self.logger = logger
+ self.email_service = email_service
def create_meal_request(
self,
@@ -125,6 +128,13 @@ def commit_to_meal_request(
raise Exception(
f'meal request "{meal_request_id}" is not open for commitment'
)
+ meal_requestor_id = meal_request.requestor.id
+ meal_requestor = User.objects(id=meal_requestor_id).first()
+
+ self.send_donor_commit_email(meal_request, donor.info.email)
+ self.send_requestor_commit_email(
+ meal_request, meal_requestor.info.email
+ )
meal_request.donation_info = DonationInfo(
donor=donor,
@@ -324,3 +334,56 @@ def get_meal_requests_by_ids(self, ids: str) -> List[MealRequestDTO]:
]
return meal_request_dtos
+
+ def send_donor_commit_email(self, meal_request, email):
+ if not self.email_service:
+ error_message = """
+ Attempted to call committed_to_meal_request but this
+ instance of AuthService does not have an EmailService instance
+ """
+ self.logger.error(error_message)
+ raise Exception(error_message)
+
+ try:
+ email_body = EmailService.read_email_template(
+ "email_templates/committed_to_meal_request.html"
+ ).format(
+ dropoff_location=meal_request.drop_off_location,
+ dropoff_time=meal_request.drop_off_datetime,
+ num_meals=meal_request.meal_info.portions,
+ )
+ self.email_service.send_email(
+ email, "Thank you for committing to a meal request!", email_body
+ )
+
+ except Exception as e:
+ self.logger.error(
+ f"Failed to send committed to meal request email for user {meal_request.id if meal_request else ''} {email}"
+ )
+ raise e
+
+ def send_requestor_commit_email(self, meal_request, email):
+ if not self.email_service:
+ error_message = """
+ Attempted to call meal_request_success but this
+ instance of AuthService does not have an EmailService instance
+ """
+ self.logger.error(error_message)
+ raise Exception(error_message)
+
+ try:
+ email_body = EmailService.read_email_template(
+ "email_templates/meal_request_success.html"
+ ).format(
+ dropoff_location=meal_request.drop_off_location,
+ dropoff_time=meal_request.drop_off_datetime,
+ num_meals=meal_request.meal_info.portions,
+ )
+ self.email_service.send_email(
+ email, "Your meal request has been fulfilled!", email_body
+ )
+ except Exception as e:
+ self.logger.error(
+ f"Failed to send committed to meal request email for user {meal_request.id if meal_request else ''} {email}"
+ )
+ raise e
diff --git a/backend/app/services/implementations/mock_email_service.py b/backend/app/services/implementations/mock_email_service.py
index 1fe006e2..950026a2 100644
--- a/backend/app/services/implementations/mock_email_service.py
+++ b/backend/app/services/implementations/mock_email_service.py
@@ -38,6 +38,9 @@ def send_email(self, to, subject, body):
"subject": subject,
"body": body,
}
+ print(
+ f"MockEmailService: Sent email to {message['to']} from {message['from_']} with subject '{message['subject']}'"
+ )
self.emails_sent.append(message)
def get_last_email_sent(self):
diff --git a/backend/app/services/implementations/reminder_email_service.py b/backend/app/services/implementations/reminder_email_service.py
new file mode 100644
index 00000000..b51b69e3
--- /dev/null
+++ b/backend/app/services/implementations/reminder_email_service.py
@@ -0,0 +1,118 @@
+from app.services.interfaces.reminder_email_service import IReminderEmailService
+from app.services.interfaces.email_service import IEmailService
+from ...models.user import User
+from ...models.meal_request import MealRequest
+from datetime import datetime, timedelta
+from app.services.implementations.email_service import EmailService
+
+
+class ReminderEmailService(IReminderEmailService):
+ def __init__(self, logger, email_service: IEmailService):
+ self.logger = logger
+ self.email_service = email_service
+
+ def get_meal_requests_one_day_away(self):
+ """
+ Helper function to get meal requests that are one day away.
+ Returns:
+ list of meal requests
+ """
+ try:
+ tomorrow_time = datetime.now() + timedelta(days=1)
+ meal_requests = MealRequest.objects(
+ drop_off_datetime__gt=tomorrow_time,
+ drop_off_datetime__lt=tomorrow_time + timedelta(hours=1),
+ )
+ except Exception as e:
+ self.logger.error("Failed to get meal requests one day away")
+ raise e
+
+ return meal_requests
+
+ def get_meal_requests_one_day_ago(self):
+ """
+ Helper function to get meal requests that are one day ago.
+ Returns:
+ list of meal requests
+ """
+ try:
+ yesterday_time = datetime.now() - timedelta(days=1)
+ meal_requests = MealRequest.objects(
+ drop_off_datetime__gt=yesterday_time - timedelta(hours=1),
+ drop_off_datetime__lt=yesterday_time,
+ )
+ except Exception as e:
+ self.logger.error("Failed to get meal requests one day ago")
+ raise e
+
+ return meal_requests
+
+ def send_email(self, email, meal_request, template_file_path, subject_line):
+ try:
+ email_body = EmailService.read_email_template(template_file_path).format(
+ dropoff_location=meal_request.drop_off_location,
+ dropoff_time=meal_request.drop_off_datetime,
+ num_meals=meal_request.meal_info.portions,
+ )
+ self.email_service.send_email(email, subject_line, email_body)
+ except Exception as e:
+ self.logger.error(
+ f"Failed to send reminder email for meal request one meal away for user {meal_request.id if meal_request else ''} {email}"
+ )
+ raise e
+ self.logger.info(f"Sent reminder email for meal request to {email}")
+
+ def send_time_delayed_emails(
+ self, meal_requests, template_file_paths, subject_lines
+ ):
+ """
+ Helper function to send emails to donors and requestors.
+ Args:
+ meal_requests: list of meal requests
+ """
+ for meal_request in meal_requests:
+ meal_requestor_email = meal_request.requestor.info.email
+ self.send_email(
+ meal_requestor_email,
+ meal_request,
+ template_file_paths["requestor"],
+ subject_lines["requestor"],
+ )
+ if hasattr(meal_request, "donation_info") and meal_request.donation_info:
+ donor_id = meal_request.donation_info.donor.id
+ donor = User.objects.get(id=donor_id)
+ donor_email = donor.info.email
+ self.send_email(
+ donor_email,
+ meal_request,
+ template_file_paths["donor"],
+ subject_lines["donor"],
+ )
+
+ def send_regularly_scheduled_emails(self):
+ """Sends scheduled emails to donors and requestors."""
+ # Send emails for meal requests that are one day away
+ self.send_time_delayed_emails(
+ self.get_meal_requests_one_day_away(),
+ {
+ "donor": "email_templates/donor_one_day_to_meal.html",
+ "requestor": "email_templates/requestor_one_day_to_meal.html",
+ },
+ {
+ "donor": "Your meal donation is only one day away!",
+ "requestor": "Your meal request is only one day away!",
+ },
+ )
+
+ # Send emails for meal requests that are one day ago
+ self.send_time_delayed_emails(
+ self.get_meal_requests_one_day_ago(),
+ {
+ "donor": "email_templates/donor_one_day_after_meal.html",
+ "requestor": "email_templates/requestor_one_day_after_meal.html",
+ },
+ {
+ "donor": "Thank you again for your meal donation!",
+ "requestor": "We hope you enjoyed your meal!",
+ },
+ )
diff --git a/backend/app/services/interfaces/meal_request_service.py b/backend/app/services/interfaces/meal_request_service.py
index 901d7a75..31e05fe0 100644
--- a/backend/app/services/interfaces/meal_request_service.py
+++ b/backend/app/services/interfaces/meal_request_service.py
@@ -134,3 +134,21 @@ def get_meal_requests_by_donor_id(
:raises Exception: if MealRequest could not be retrieved
"""
pass
+
+ @abstractmethod
+ def send_donor_commit_email(self, meal_request_id, email):
+ """
+ Sends an email to the user with the given email, notifying them that they have committed to a meal request
+ :param meal_request_id: the id of the meal request
+ :type email: str
+ :raises Exception: if unable to send email
+ """
+
+ @abstractmethod
+ def send_requestor_commit_email(self, meal_request_id, email):
+ """
+ Sends an email to the user with the given email, notifying them that their meal request was successful
+ :param meal_request_id: the id of the meal request
+ :type email: str
+ :raises Exception: if unable to send email
+ """
diff --git a/backend/app/services/interfaces/reminder_email_service.py b/backend/app/services/interfaces/reminder_email_service.py
new file mode 100644
index 00000000..9dd88bf7
--- /dev/null
+++ b/backend/app/services/interfaces/reminder_email_service.py
@@ -0,0 +1,11 @@
+from abc import ABC
+
+from app.services.interfaces.email_service import IEmailService
+
+
+class IReminderEmailService(ABC):
+ def __init__(self, logger, email_service: IEmailService):
+ pass
+
+ def send_regularly_scheduled_emails(self):
+ pass
diff --git a/backend/email_templates/committed_to_meal_request.html b/backend/email_templates/committed_to_meal_request.html
new file mode 100644
index 00000000..582d22ce
--- /dev/null
+++ b/backend/email_templates/committed_to_meal_request.html
@@ -0,0 +1,11 @@
+Hello,
+Thank you for committing to a meal request!
+
+The information of the meal request is as follows:
+
+
+Dropoff Location: {dropoff_location}
+
+Dropoff Time: {dropoff_time}
+
+Number of Meals: {num_meals}
diff --git a/backend/email_templates/donor_one_day_after_meal.html b/backend/email_templates/donor_one_day_after_meal.html
new file mode 100644
index 00000000..cd2404da
--- /dev/null
+++ b/backend/email_templates/donor_one_day_after_meal.html
@@ -0,0 +1,5 @@
+Hello,
+Thank you again for your donation!
+Your meal request was supplied yesterday.
+
+Thank you again for the donation!
diff --git a/backend/email_templates/donor_one_day_to_meal.html b/backend/email_templates/donor_one_day_to_meal.html
new file mode 100644
index 00000000..14f3eeec
--- /dev/null
+++ b/backend/email_templates/donor_one_day_to_meal.html
@@ -0,0 +1,13 @@
+Hello,
+The meal request you donated is scheduled for tomorrow!
+
+The information of the meal request is as follows:
+
+
+Dropoff Location: {dropoff_location}
+
+Dropoff Time: {dropoff_time}
+
+Number of Meals: {num_meals}
+
+Thank you for the donation!
diff --git a/backend/email_templates/meal_request_success.html b/backend/email_templates/meal_request_success.html
new file mode 100644
index 00000000..8ce58a63
--- /dev/null
+++ b/backend/email_templates/meal_request_success.html
@@ -0,0 +1,11 @@
+Hello,
+Your meal request has been fulfilled!
+
+The information of the meal request is as follows:
+
+
+Dropoff Location: {dropoff_location}
+
+Dropoff Time: {dropoff_time}
+
+Number of Meals: {num_meals}
diff --git a/backend/email_templates/onboarding_request_approved.html b/backend/email_templates/onboarding_request_approved.html
new file mode 100644
index 00000000..80f6d511
--- /dev/null
+++ b/backend/email_templates/onboarding_request_approved.html
@@ -0,0 +1,5 @@
+Hello,
+We have received your onboarding request and it has been approved. Please set
+your password using the following link.
+
+Reset Password
diff --git a/backend/email_templates/onboarding_request_rejected.html b/backend/email_templates/onboarding_request_rejected.html
new file mode 100644
index 00000000..95552cec
--- /dev/null
+++ b/backend/email_templates/onboarding_request_rejected.html
@@ -0,0 +1,3 @@
+Hello,
+This is a notification that your onboarding request has been rejected.
+
diff --git a/backend/email_templates/requestor_one_day_after_meal.html b/backend/email_templates/requestor_one_day_after_meal.html
new file mode 100644
index 00000000..12bc7797
--- /dev/null
+++ b/backend/email_templates/requestor_one_day_after_meal.html
@@ -0,0 +1,3 @@
+Hello,
+We hope you enjoyed your requested meal!
+Please let us know if you have any feedback or suggestions for us.
diff --git a/backend/email_templates/requestor_one_day_to_meal.html b/backend/email_templates/requestor_one_day_to_meal.html
new file mode 100644
index 00000000..54c154cd
--- /dev/null
+++ b/backend/email_templates/requestor_one_day_to_meal.html
@@ -0,0 +1,11 @@
+Hello,
+Your meal request is scheduled for tomorrow!
+
+The information of the meal request is as follows:
+
+
+Dropoff Location: {dropoff_location}
+
+Dropoff Time: {dropoff_time}
+
+Number of Meals: {num_meals}
diff --git a/backend/email_templates/reset_password.html b/backend/email_templates/reset_password.html
new file mode 100644
index 00000000..8f99720d
--- /dev/null
+++ b/backend/email_templates/reset_password.html
@@ -0,0 +1,5 @@
+Hello,
+We have received your reset password request. Please reset your password using
+the following link.
+
+Reset Password
diff --git a/backend/email_templates/verification_email.html b/backend/email_templates/verification_email.html
new file mode 100644
index 00000000..a1ef5317
--- /dev/null
+++ b/backend/email_templates/verification_email.html
@@ -0,0 +1,9 @@
+
Hello,
+
+
+ Please click the following link to verify your email and activate your
+ account.
+ This link is only valid for 1 hour.
+
+ Verify email
+