Coverage for app/utils.py: 89%
64 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-26 10:48 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-26 10:48 +0000
1import logging
2from dataclasses import dataclass
3from datetime import UTC, datetime, timedelta
4from pathlib import Path
5from typing import Any
7import emails
8import jwt
9from jinja2 import Template
10from jwt.exceptions import InvalidTokenError
12from app.core import security
13from app.core.config import settings
15logging.basicConfig(level=logging.INFO)
16logger = logging.getLogger(__name__)
19@dataclass
20class EmailData:
21 html_content: str
22 subject: str
25def render_email_template(*, template_name: str, context: dict[str, Any]) -> str:
26 template_str = ( 1bd
27 Path(__file__).parent / "email-templates" / template_name
28 ).read_text()
29 template: Template = Template(template_str) 1bd
30 html_content = template.render(context)
31 return html_content
34def send_email(
35 *,
36 email_to: str,
37 subject: str = "",
38 html_content: str = "",
39) -> None:
40 assert settings.emails_enabled, "no provided configuration for email variables"
41 assert settings.EMAILS_FROM_EMAIL # For type checker
42 message = emails.message.Message(
43 subject=subject,
44 html=html_content,
45 mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
46 )
47 smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT}
48 if settings.SMTP_TLS:
49 smtp_options["tls"] = True
50 elif settings.SMTP_SSL:
51 smtp_options["ssl"] = True
52 if settings.SMTP_USER:
53 smtp_options["user"] = settings.SMTP_USER
54 if settings.SMTP_PASSWORD:
55 smtp_options["password"] = settings.SMTP_PASSWORD
56 response = message.send(to=email_to, smtp=smtp_options)
57 logger.info(f"send email result: {response}")
60def generate_test_email(email_to: str) -> EmailData:
61 project_name = settings.PROJECT_NAME
62 subject = f"{project_name} - Test email"
63 html_content = render_email_template(
64 template_name="test_email.html",
65 context={"project_name": settings.PROJECT_NAME, "email": email_to},
66 )
67 return EmailData(html_content=html_content, subject=subject)
70def generate_reset_password_email(email_to: str, email: str, token: str) -> EmailData:
71 project_name = settings.PROJECT_NAME 1b
72 subject = f"{project_name} - Password recovery for user {email}" 1b
73 link = f"{settings.FRONTEND_HOST}/reset-password?token={token}" 1b
74 html_content = render_email_template( 1b
75 template_name="reset_password.html",
76 context={
77 "project_name": settings.PROJECT_NAME,
78 "username": email,
79 "email": email_to,
80 "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
81 "link": link,
82 },
83 )
84 return EmailData(html_content=html_content, subject=subject)
87def generate_new_account_email(
88 email_to: str, username: str, password: str
89) -> EmailData:
90 project_name = settings.PROJECT_NAME 1d
91 subject = f"{project_name} - New account for user {username}" 1d
92 html_content = render_email_template( 1d
93 template_name="new_account.html",
94 context={
95 "project_name": settings.PROJECT_NAME,
96 "username": username,
97 "password": password,
98 "email": email_to,
99 "link": settings.FRONTEND_HOST,
100 },
101 )
102 return EmailData(html_content=html_content, subject=subject)
105def generate_password_reset_token(email: str) -> str:
106 delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS) 1bc
107 now = datetime.now(UTC) 1bc
108 expires = now + delta 1bc
109 exp = expires.timestamp() 1bc
110 encoded_jwt = jwt.encode( 1bc
111 {"exp": exp, "nbf": now, "sub": email},
112 settings.SECRET_KEY,
113 algorithm=security.ALGORITHM,
114 )
115 return encoded_jwt 1bc
118def verify_password_reset_token(token: str) -> str | None:
119 try: 1ce
120 decoded_token = jwt.decode( 1ce
121 token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
122 )
123 return str(decoded_token["sub"]) 1c
124 except InvalidTokenError: 1e
125 return None 1e