diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..879b9bf --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USERNAME=your_username_here +SMTP_PASSWORD=your_password_here +SMTP_FROM=no-reply@example.com +SMTP_TO=recipient@example.com +SMTP_USE_TLS=false +SMTP_USE_SSL=false \ No newline at end of file diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..1233a15 --- /dev/null +++ b/core/config.py @@ -0,0 +1,15 @@ +import os + + +def _env_bool(name: str, default: str = "false") -> bool: + return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"} + + +SMTP_HOST = os.getenv("SMTP_HOST", "localhost") +SMTP_PORT = int(os.getenv("SMTP_PORT", "25")) +SMTP_USERNAME = os.getenv("SMTP_USERNAME", "") +SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") +SMTP_FROM = os.getenv("SMTP_FROM", "") +SMTP_TO = os.getenv("SMTP_TO", "") +SMTP_USE_TLS = _env_bool("SMTP_USE_TLS") +SMTP_USE_SSL = _env_bool("SMTP_USE_SSL") diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/routers/submit.py b/routers/submit.py index f6745ea..c8c46f8 100644 --- a/routers/submit.py +++ b/routers/submit.py @@ -9,6 +9,7 @@ from models.dto import ExecAppFormDTO from models.request import ExecFormRequest from models.response import ResponseDTO +from services.smtp_email_sender import send_application_email router = APIRouter(prefix="/api/submit", tags=["submit"]) @@ -106,6 +107,17 @@ async def receive_exec_form(form_dto: ExecAppFormDTO): processed_questions, ) + send_application_email( + full_name=exec_form_request.fullName, + sfu_email=exec_form_request.email, + student_id=exec_form_request.studentId, + year_of_study=exec_form_request.yearOfStudy, + faculty=exec_form_request.faculty, + major=exec_form_request.major, + co_op=exec_form_request.isCoop, + team_type=team_type_str, + ) + return ResponseDTO( message="Submission received successfully", content=exec_form_request.model_dump(mode="json"), @@ -115,3 +127,4 @@ async def receive_exec_form(form_dto: ExecAppFormDTO): @router.post("/project-proposal") async def receive_project_proposal_form(payload: Dict[str, Any]): return JSONResponse(status_code=400, content="Under Construction") + diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/smtp_email_sender.py b/services/smtp_email_sender.py new file mode 100644 index 0000000..f513021 --- /dev/null +++ b/services/smtp_email_sender.py @@ -0,0 +1,72 @@ +from email.message import EmailMessage +import smtplib +from core.config import SMTP_FROM, SMTP_TO +from core.config import SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, SMTP_USE_SSL + + +def build_email_message( + full_name: str, + sfu_email: str, + student_id: int, + year_of_study: int, + faculty: str, + major: str, + co_op: bool, + team_type: str, +) -> EmailMessage: + msg = EmailMessage() + msg["Subject"] = f"New Executive Application: {full_name} - {team_type}" + msg["From"] = SMTP_FROM + msg["To"] = SMTP_TO + msg["Reply-To"] = sfu_email + + msg.set_content(f"""\ +Full Name: {full_name} +SFU Email: {sfu_email} +Student ID: {student_id} +Year of Study: {year_of_study} +Major: {major} +Faculty: {faculty} +Co-op: {"Yes" if co_op else "No"} +Team Type: {team_type} +""") + + return msg + +def send_email_message(msg: EmailMessage) -> None: + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: + if SMTP_USERNAME and SMTP_PASSWORD: + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: + if SMTP_USE_TLS: + server.starttls() + if SMTP_USERNAME and SMTP_PASSWORD: + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + +def send_application_email( + full_name: str, + sfu_email: str, + student_id: int, + year_of_study: int, + faculty: str, + major: str, + co_op: bool, + team_type: str, +) -> None: + msg = build_email_message( + full_name=full_name, + sfu_email=sfu_email, + student_id=student_id, + year_of_study=year_of_study, + faculty=faculty, + major=major, + co_op=co_op, + team_type=team_type + ) + send_email_message(msg) + + diff --git a/tests/test_submit.py b/tests/test_submit.py index 254a13a..d8180ef 100644 --- a/tests/test_submit.py +++ b/tests/test_submit.py @@ -1,4 +1,5 @@ -# import pytest +from unittest.mock import patch + from fastapi.testclient import TestClient from main import app @@ -6,7 +7,8 @@ client = TestClient(app) -def test_submit_exec_form_success(): +@patch("routers.submit.send_application_email") +def test_submit_exec_form_success(mock_send_application_email): # Test data for a successful submission test_data = { "fullName": "John Doe", @@ -28,9 +30,20 @@ def test_submit_exec_form_success(): assert "message" in data assert data["message"] == "Submission received successfully" assert "content" in data - - -def test_submit_exec_form_invalid_student_id(): + mock_send_application_email.assert_called_once_with( + full_name="John Doe", + sfu_email="john.doe@example.com", + student_id=12345678, + year_of_study=3, + faculty="Engineering", + major="Computer Science", + co_op=False, + team_type="EVENT", + ) + + +@patch("routers.submit.send_application_email") +def test_submit_exec_form_invalid_student_id(mock_send_application_email): # Test with invalid student ID test_data = { "fullName": "Jane Doe", @@ -50,9 +63,11 @@ def test_submit_exec_form_invalid_student_id(): data = response.json() assert "message" in data assert "Invalid number format" in data["message"] + mock_send_application_email.assert_not_called() -def test_submit_exec_form_coop_term(): +@patch("routers.submit.send_application_email") +def test_submit_exec_form_coop_term(mock_send_application_email): # Test with coop term yes test_data = { "fullName": "Alice Smith", @@ -72,9 +87,11 @@ def test_submit_exec_form_coop_term(): data = response.json() assert "message" in data assert "co-op term" in data["message"] + mock_send_application_email.assert_not_called() -def test_submit_exec_form_invalid_team(): +@patch("routers.submit.send_application_email") +def test_submit_exec_form_invalid_team(mock_send_application_email): # Test with invalid team type test_data = { "fullName": "Bob Johnson", @@ -94,8 +111,11 @@ def test_submit_exec_form_invalid_team(): data = response.json() assert "message" in data assert "Invalid team type" in data["message"] + mock_send_application_email.assert_not_called() + -def test_submit_exec_form_missing_fields(): +@patch("routers.submit.send_application_email") +def test_submit_exec_form_missing_fields(mock_send_application_email): # Test for when one component is missing test_data = { "fullName": "Charlie Brown", @@ -114,3 +134,4 @@ def test_submit_exec_form_missing_fields(): data = response.json() assert "message" in data assert "Missing required fields" in data["message"] + mock_send_application_email.assert_not_called()