Skip to content

Commit 17c676f

Browse files
YashK2005claude
andcommitted
Implement bilingual email template system with Jinja2
- Reorganized email templates into structured folders (base/, source/, compiled/, text/) - Created Jinja2-based template system with base templates for EN and FR - Implemented 7 email types in both English and French (14 total): * Email verification * Password reset * Intake form confirmation * Matches available * Call scheduled * Participant requested new times (volunteer-only) * Volunteer accepted new times (participant-only) - Added dynamic parameters to email subjects and bodies - Updated SES email service to support language parameter for all email methods - Updated auth service to retrieve user's first name from database (with Firebase fallback) for personalized password reset emails - Created custom Jinja2 PreservingUndefined class to preserve {{}} syntax for AWS SES variable replacement - Updated update_ses_templates.py script to compile .j2 source files to HTML - Fixed logo URLs to use PNG format for Gmail compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 0e2d621 commit 17c676f

File tree

58 files changed

+4103
-163
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+4103
-163
lines changed

backend/app/services/implementations/auth_service.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ def renew_token(self, refresh_token: str) -> Token:
4949

5050
def reset_password(self, email: str) -> None:
5151
try:
52+
# Get user's first name if available
53+
first_name = None
54+
try:
55+
# Try database first, then fall back to Firebase
56+
try:
57+
user = self.user_service.get_user_by_email(email)
58+
if user and user.first_name and user.first_name.strip():
59+
first_name = user.first_name.strip()
60+
except Exception:
61+
pass
62+
63+
# Fall back to Firebase if database didn't have first_name
64+
if not first_name:
65+
try:
66+
firebase_user = firebase_admin.auth.get_user_by_email(email)
67+
if firebase_user and firebase_user.display_name:
68+
display_name = firebase_user.display_name.strip()
69+
first_name = display_name.split()[0] if display_name else None
70+
except Exception:
71+
pass
72+
except Exception:
73+
pass
74+
5275
# Use Firebase Admin SDK to generate password reset link
5376
action_code_settings = firebase_admin.auth.ActionCodeSettings(
5477
url="http://localhost:3000/set-new-password",
@@ -58,7 +81,7 @@ def reset_password(self, email: str) -> None:
5881
reset_link = firebase_admin.auth.generate_password_reset_link(email, action_code_settings)
5982

6083
# Send via SES
61-
email_sent = self.ses_email_service.send_password_reset_email(email, reset_link)
84+
email_sent = self.ses_email_service.send_password_reset_email(email, reset_link, first_name)
6285

6386
if email_sent:
6487
self.logger.info(f"Password reset email sent successfully to {email}")
Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
11
[
22
{
3-
"HtmlPart": "app/utilities/ses/template_files/test.html",
3+
"HtmlPart": "app/utilities/ses/template_files/compiled/test.html",
44
"SubjectPart": "Testing Email SES Template",
55
"TemplateName": "Test",
6-
"TextPart": "app/utilities/ses/template_files/test.txt"
6+
"TextPart": "app/utilities/ses/template_files/text/test.txt"
77
},
88
{
9-
"HtmlPart": "app/utilities/ses/template_files/email_verification_en.html",
10-
"SubjectPart": "Verify Your Email Address",
9+
"HtmlPart": "app/utilities/ses/template_files/compiled/email_verification_en.html",
10+
"SubjectPart": "{{first_name}}, confirm your email - First Connection Peer Support Program",
1111
"TemplateName": "EmailVerificationEn",
12-
"TextPart": "app/utilities/ses/template_files/email_verification_en.txt"
12+
"TextPart": "app/utilities/ses/template_files/text/email_verification_en.txt"
1313
},
1414
{
15-
"HtmlPart": "app/utilities/ses/template_files/email_verification_fr.html",
16-
"SubjectPart": "Vérifiez votre adresse courriel",
15+
"HtmlPart": "app/utilities/ses/template_files/compiled/email_verification_fr.html",
16+
"SubjectPart": "{{first_name}}, confirmation de l'adresse courriel – Programme de soutien par les pairs Premier contact",
1717
"TemplateName": "EmailVerificationFr",
18-
"TextPart": "app/utilities/ses/template_files/email_verification_fr.txt"
18+
"TextPart": "app/utilities/ses/template_files/text/email_verification_fr.txt"
1919
},
2020
{
21-
"HtmlPart": "app/utilities/ses/template_files/password_reset.html",
22-
"SubjectPart": "Reset Your Password",
23-
"TemplateName": "PasswordReset",
24-
"TextPart": "app/utilities/ses/template_files/password_reset.txt"
21+
"HtmlPart": "app/utilities/ses/template_files/compiled/password_reset_en.html",
22+
"SubjectPart": "Reset Your Password - First Connection Peer Support Program",
23+
"TemplateName": "PasswordResetEn",
24+
"TextPart": "app/utilities/ses/template_files/text/password_reset_en.txt"
25+
},
26+
{
27+
"HtmlPart": "app/utilities/ses/template_files/compiled/password_reset_fr.html",
28+
"SubjectPart": "Réinitialisation du mot de passe – Programme de soutien par les pairs Premier contact",
29+
"TemplateName": "PasswordResetFr",
30+
"TextPart": "app/utilities/ses/template_files/text/password_reset_fr.txt"
31+
},
32+
{
33+
"HtmlPart": "app/utilities/ses/template_files/compiled/intake_form_confirmation_en.html",
34+
"SubjectPart": "We received your intake form - First Connection Peer Support Program",
35+
"TemplateName": "IntakeFormConfirmationEn",
36+
"TextPart": "app/utilities/ses/template_files/text/intake_form_confirmation_en.txt"
37+
},
38+
{
39+
"HtmlPart": "app/utilities/ses/template_files/compiled/intake_form_confirmation_fr.html",
40+
"SubjectPart": "Réception de votre formulaire de demande – Programme de soutien par les pairs Premier contact",
41+
"TemplateName": "IntakeFormConfirmationFr",
42+
"TextPart": "app/utilities/ses/template_files/text/intake_form_confirmation_fr.txt"
43+
},
44+
{
45+
"HtmlPart": "app/utilities/ses/template_files/compiled/matches_available_en.html",
46+
"SubjectPart": "{{first_name}}, you have new matches - First Connection Peer Support Program",
47+
"TemplateName": "MatchesAvailableEn",
48+
"TextPart": "app/utilities/ses/template_files/text/matches_available_en.txt"
49+
},
50+
{
51+
"HtmlPart": "app/utilities/ses/template_files/compiled/matches_available_fr.html",
52+
"SubjectPart": "{{first_name}}, nouveaux jumelages – Programme de soutien par les pairs Premier contact",
53+
"TemplateName": "MatchesAvailableFr",
54+
"TextPart": "app/utilities/ses/template_files/text/matches_available_fr.txt"
55+
},
56+
{
57+
"HtmlPart": "app/utilities/ses/template_files/compiled/call_scheduled_en.html",
58+
"SubjectPart": "Call confirmed with {{match_name}} @ {{date}} {{time}} {{timezone}} - First Connection Peer Support Program",
59+
"TemplateName": "CallScheduledEn",
60+
"TextPart": "app/utilities/ses/template_files/text/call_scheduled_en.txt"
61+
},
62+
{
63+
"HtmlPart": "app/utilities/ses/template_files/compiled/call_scheduled_fr.html",
64+
"SubjectPart": "Confirmation de l'appel avec {{match_name}} le {{date}} à {{time}} {{timezone}} – Programme de soutien par les pairs Premier contact",
65+
"TemplateName": "CallScheduledFr",
66+
"TextPart": "app/utilities/ses/template_files/text/call_scheduled_fr.txt"
67+
},
68+
{
69+
"HtmlPart": "app/utilities/ses/template_files/compiled/participant_requested_new_times_en.html",
70+
"SubjectPart": "{{participant_name}} requested new times - First Connection Peer Support Program",
71+
"TemplateName": "ParticipantRequestedNewTimesEn",
72+
"TextPart": "app/utilities/ses/template_files/text/participant_requested_new_times_en.txt"
73+
},
74+
{
75+
"HtmlPart": "app/utilities/ses/template_files/compiled/participant_requested_new_times_fr.html",
76+
"SubjectPart": "Demande d'une autre plage horaire par {{participant_name}} – Programme de soutien par les pairs Premier contact",
77+
"TemplateName": "ParticipantRequestedNewTimesFr",
78+
"TextPart": "app/utilities/ses/template_files/text/participant_requested_new_times_fr.txt"
79+
},
80+
{
81+
"HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_accepted_new_times_en.html",
82+
"SubjectPart": "{{volunteer_name}} confirmed your new time @ {{date}} {{time}} {{timezone}} - First Connection Peer Support Program",
83+
"TemplateName": "VolunteerAcceptedNewTimesEn",
84+
"TextPart": "app/utilities/ses/template_files/text/volunteer_accepted_new_times_en.txt"
85+
},
86+
{
87+
"HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_accepted_new_times_fr.html",
88+
"SubjectPart": "Confirmation de la nouvelle plage horaire, le {{date}} à {{time}} {{timezone}}, par {{volunteer_name}} – Programme de soutien par les pairs Premier contact",
89+
"TemplateName": "VolunteerAcceptedNewTimesFr",
90+
"TextPart": "app/utilities/ses/template_files/text/volunteer_accepted_new_times_fr.txt"
2591
}
2692
]

backend/app/utilities/ses/template_files/email_verification.html renamed to backend/app/utilities/ses/template_files/base/base_email_en.html

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<td
1111
width="100%"
1212
valign="top"
13-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359bodyCell"
13+
id="bodyCell"
1414
style="width:580px;padding:7.5pt;font-family:Arial,sans-serif;font-size:14px;line-height:1.5;color:#000000;"
1515
>
1616
<div align="center">
@@ -23,27 +23,31 @@
2323
<tr>
2424
<td
2525
valign="top"
26-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359templateUpperBody"
26+
id="templateUpperBody"
2727
style="background-image:initial;background-position:initial;background-size:initial;background-repeat:initial;background-origin:initial;background-clip:initial;padding:0in"
2828
>
2929
<table border="0" cellspacing="0" cellpadding="0" width="100%" style="width:580px;border-collapse:collapse;min-width:100%">
3030
<tbody>
3131
<tr>
3232
<td style="padding:0in;min-width:100%">
33-
<div style="text-align:center">
34-
<img
35-
src="https://www.bloodcancers.ca/themes/custom/whirlwind-llsc/src/000_assets/logos/llsc-logo-en.svg"
36-
alt="llsc_en_small.png"
37-
width="120"
38-
height="71"
39-
style="margin-right: 0px;"
40-
><br>
41-
</div>
33+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin:0 auto 20px auto;">
34+
<tr>
35+
<td align="center" style="padding:0;">
36+
<img
37+
src="https://secure.llscanada.org/images/content/pagebuilder/logo-lls_en.png"
38+
alt="LLSC Logo"
39+
width="120"
40+
height="71"
41+
border="0"
42+
style="display:block;margin:0 auto;max-width:120px;height:auto;width:120px;outline:none;text-decoration:none;border:none;-ms-interpolation-mode:bicubic;"
43+
>
44+
</td>
45+
</tr>
46+
</table>
4247

43-
<p class="MsoNormal">Hi {{first_name}},</p>
44-
<p>Click the link below to verify your email address: <a href="{{verification_link}}">{{verification_link}}</a></p>
45-
<p>If the link doesn&#39;t work, copy and paste it into your browser.</p>
46-
<p>This link will expire in 24 hours. If you didn&#39;t request this verification, please ignore this email.</p>
48+
{% block content %}
49+
<!-- Email content goes here -->
50+
{% endblock %}
4751

4852
<div class="MsoNormal" align="center" style="margin-top:15pt;text-align:center">
4953
<hr size="2" width="100%" align="center">
@@ -73,10 +77,8 @@
7377
border="0"
7478
width="25"
7579
height="25"
76-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1044"
7780
src="http://secure.llscanada.org/images/content/pagebuilder/instagram-logo.png"
7881
alt="Instagram"
79-
class="gmail-CToWUd"
8082
style="width: 0.2604in; height: 0.2604in;"
8183
>
8284
</span>
@@ -92,10 +94,8 @@
9294
border="0"
9395
width="25"
9496
height="25"
95-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1043"
9697
src="http://secure.llscanada.org/images/content/pagebuilder/facebook-logo.png"
9798
alt="Facebook"
98-
class="gmail-CToWUd"
9999
style="width: 0.2604in; height: 0.2604in;"
100100
>
101101
</span>
@@ -111,10 +111,8 @@
111111
border="0"
112112
width="25"
113113
height="25"
114-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1042"
115114
src="http://secure.llscanada.org/images/content/pagebuilder/linkedin-logo.png"
116115
alt="LinkedIn"
117-
class="gmail-CToWUd"
118116
style="width: 0.2604in; height: 0.2604in;"
119117
>
120118
</span>
@@ -130,10 +128,8 @@
130128
border="0"
131129
width="25"
132130
height="25"
133-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1041"
134131
src="http://secure.llscanada.org/images/content/pagebuilder/YouTube_logo.png"
135132
alt="YouTube"
136-
class="gmail-CToWUd"
137133
style="width: 0.2604in; height: 0.2604in;"
138134
>
139135
</span>
@@ -149,10 +145,8 @@
149145
border="0"
150146
width="25"
151147
height="25"
152-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1040"
153148
src="http://secure.llscanada.org/images/content/pagebuilder/twitter-x.jpg"
154149
alt="X"
155-
class="gmail-CToWUd"
156150
style="width: 0.2604in; height: 0.2604in;"
157151
>
158152
</span>
@@ -168,10 +162,8 @@
168162
border="0"
169163
width="25"
170164
height="25"
171-
id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1039"
172165
src="http://secure.llscanada.org/images/content/pagebuilder/spotify-logo.png"
173166
alt="Spotify"
174-
class="gmail-CToWUd"
175167
style="width: 0.2604in; height: 0.2604in;"
176168
>
177169
</span>
@@ -211,7 +203,7 @@
211203
</div>
212204

213205
NOTICE: Please do not reply to this email. It is automated from an unmonitored email address and will not be received or responded to. Please contact
214-
<a href="mailto:[email protected]">[email protected]</a> for any questions or concerns related to the First Connection Peer Support Program.
206+
<a href="mailto:[email protected]">[email protected]</a> for any questions or concerns related to the First Connection Peer Support Program.
215207

216208
</td>
217209
</tr>

0 commit comments

Comments
 (0)