Coverage for utils/email_templates.py: 100.00%

13 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-03 15:30 +0000

1from typing import Any 

2 

3 

4class EmailTemplate: 

5 """Email template with subject, plain text body, and optional HTML body""" 

6 

7 def __init__(self, subject: str, body: str, html_body: str | None = None): 

8 self.subject = subject 

9 self.body = body 

10 self.html_body = html_body 

11 

12 def render(self, **kwargs: Any) -> dict[str, str]: 

13 """ 

14 Render template with variables. 

15 

16 Args: 

17 **kwargs: Variables to substitute in template 

18 

19 Returns: 

20 Dict with 'subject', 'body', and optionally 'html_body' keys 

21 """ 

22 result = { 

23 "subject": self.subject.format(**kwargs), 

24 "body": self.body.format(**kwargs), 

25 } 

26 

27 if self.html_body: 

28 result["html_body"] = self.html_body.format(**kwargs) 

29 

30 return result 

31 

32 

33# Password Reset Email Template 

34PASSWORD_RESET_TEMPLATE = EmailTemplate( 

35 subject="Reset your password - {app_name}", 

36 body=( 

37 "Hi {user_name},\n\n" 

38 "You requested a password reset for your {app_name} account.\n\n" 

39 "Please click the link below to set a new password:\n{reset_url}\n\n" 

40 "This link will expire in 30 minutes.\n\n" 

41 "If you did not request this, you can safely ignore this email." 

42 ), 

43 html_body=( 

44 "<!DOCTYPE html>" 

45 "<html>" 

46 "<head>" 

47 "<meta charset='utf-8'>" 

48 "<meta name='viewport' content='width=device-width, initial-scale=1.0'>" 

49 "</head>" 

50 '<body style=\'margin: 0; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #ffffff;\'>' 

51 "<div style='max-width: 80dvw; margin: 0 auto;'>" 

52 "<h3 style='margin: 0 0 16px; color: #212529; line-height: 1.6; font-weight: 500;'>Hi {user_name},</h3>" 

53 "<p style='margin: 0 0 20px; color: #212529; font-size: 16px; line-height: 1.6;'>" 

54 "You requested a password reset for your <strong>{app_name}</strong> account. <br>Click the button below to set a new password. This link will expire in <strong>30 minutes</strong>." 

55 "</p>" 

56 "<p style='margin:0 0 28px; user-select: none;'>" 

57 "<a href='{reset_url}' " 

58 "style='display: inline-block; background-color: #212529; " 

59 "color: #ffffff; text-decoration: none; padding: 10px 16px; border-radius: 12px; " 

60 "font-size: 16px; font-weight: 500;'" 

61 ">" 

62 "Reset Password" 

63 "</a>" 

64 "</p>" 

65 "<p style='width: fit-content; margin: 0; padding: 10px 18px; border-radius: 12px; border: 1px solid #e9ecef; background-color: #f8f9fa; color: #495057; font-size: 14px; line-height: 1.6;'>" 

66 "If you did not request this password reset, you can safely ignore this email. Your account remains secure." 

67 "</p>" 

68 "</div>" 

69 "</body>" 

70 "</html>" 

71 ), 

72) 

73 

74# Email Verification Template 

75EMAIL_VERIFICATION_TEMPLATE = EmailTemplate( 

76 subject="Verify your email - {app_name}", 

77 body=( 

78 "Hi {user_name},\n\n" 

79 "Please verify your email address for your {app_name} account.\n\n" 

80 "Please click the link below to verify your email address:\n{verification_url}\n\n" 

81 "This link will expire in {expire_minutes} minutes.\n\n" 

82 "If you did not request this email, you can safely ignore it." 

83 ), 

84 html_body=( 

85 "<!DOCTYPE html>" 

86 "<html>" 

87 "<head>" 

88 "<meta charset='utf-8'>" 

89 "<meta name='viewport' content='width=device-width, initial-scale=1.0'>" 

90 "</head>" 

91 '<body style=\'margin: 0; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #ffffff;\'>' 

92 "<div style='max-width: 80dvw; margin: 0 auto;'>" 

93 "<h3 style='margin: 0 0 16px; color: #212529; line-height: 1.6; font-weight: 500;'>Hi {user_name},</h3>" 

94 "<p style='margin: 0 0 20px; color: #212529; font-size: 16px; line-height: 1.6;'>" 

95 "Please verify your email address for your <strong>{app_name}</strong> account. <br>Please click the button below to verify your email address. This link will expire in <strong>{expire_minutes} minutes</strong>." 

96 "</p>" 

97 "<p style='margin:0 0 28px; user-select: none;'>" 

98 "<a href='{verification_url}' " 

99 "style='display: inline-block; background-color: #212529; " 

100 "color: #ffffff; text-decoration: none; padding: 10px 16px; border-radius: 12px; " 

101 "font-size: 16px; font-weight: 500;'" 

102 ">" 

103 "Verify Email" 

104 "</a>" 

105 "</p>" 

106 "<p style='width: fit-content; margin: 0; padding: 10px 18px; border-radius: 12px; border: 1px solid #e9ecef; background-color: #f8f9fa; color: #495057; font-size: 14px; line-height: 1.6;'>" 

107 "If you did not request this email, you can safely ignore it." 

108 "</p>" 

109 "</div>" 

110 "</body>" 

111 "</html>" 

112 ), 

113)