diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py
index c13c5bf96bd8..ee5a7fc6aeae 100644
--- a/common/djangoapps/student/tasks.py
+++ b/common/djangoapps/student/tasks.py
@@ -2,6 +2,8 @@
Celery task for course enrollment email
"""
import logging
+from datetime import datetime
+
from celery import shared_task
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -29,6 +31,76 @@
MAX_RETRIES = 3
+def _build_enrollment_email_image_urls(language='en'):
+ """
+ Build absolute URLs for enrollment email images.
+
+ This function constructs full image URLs for SES email delivery. When Braze renders
+ the email through SES, it needs absolute URLs because email clients cannot resolve
+ relative paths or Django static file paths.
+
+ Args:
+ language (str): Language code ('en' or 'es') to select correct image variants
+
+ Returns:
+ dict: Mapping of image variable names to absolute URLs
+
+ How it works:
+ 1. settings.LMS_ROOT_URL is evaluated at runtime (e.g., 'https://courses.edx.org')
+ 2. f-string interpolates this value into the path
+ 3. Email client receives complete URL: 'https://courses.edx.org/static/images/enrollment_email/...'
+ 4. Browser/email client can fetch image directly without context about Django
+
+ Example flow:
+ - Python (settings): LMS_ROOT_URL = 'https://courses.edx.org'
+ - f-string: f"{settings.LMS_ROOT_URL}/static/images/enrollment_email/person_icon_{language}.png"
+ - Result: 'https://courses.edx.org/static/images/enrollment_email/person_icon_en.png'
+ - Email client: Makes HTTP GET to that URL, renders image
+ """
+ lms_root = configuration_helpers.get_value(
+ "LMS_ROOT_URL", settings.LMS_ROOT_URL
+ ).rstrip('/')
+
+ # NOTE: Image URLs for SES templates.
+ # Currently not used in Braze flow — will be enabled during SES migration.
+ # Construct image URLs based on language
+ image_urls = {
+ 'logo_url': f"{lms_root}/static/images/edx_logo.png",
+ 'you_are_enrolled_en': f"{lms_root}/static/images/enrollment_email/you_are_enrolled_en.png",
+ 'banner_default': f"{lms_root}/static/images/enrollment_email/banner_default.png",
+ 'timer_icon_en': f"{lms_root}/static/images/enrollment_email/timer_icon_en.png",
+ 'person_icon_en': f"{lms_root}/static/images/enrollment_email/person_icon_en.png",
+ 'dollar_icon_en': f"{lms_root}/static/images/enrollment_email/dollar_icon_en.png",
+ 'goal_idea_icon_en': f"{lms_root}/static/images/enrollment_email/goal_idea_icon_en.png",
+ 'flag_icon_pink_en': f"{lms_root}/static/images/enrollment_email/flag_icon_pink_en.png",
+ 'flag_icon_black_en_es': f"{lms_root}/static/images/enrollment_email/flag_icon_black_en_es.png",
+ 'flag_icon_orange_en': f"{lms_root}/static/images/enrollment_email/flag_icon_orange_en.png",
+ 'vertical_line_white_en': f"{lms_root}/static/images/enrollment_email/vertical_line_white_en.png",
+ 'vertical_line_orange_en': f"{lms_root}/static/images/enrollment_email/vertical_line_orange_en.png",
+ 'vertical_line_black_en': f"{lms_root}/static/images/enrollment_email/vertical_line_black_en.png",
+ 'community_illustration_en': f"{lms_root}/static/images/enrollment_email/community_illustration_en.png",
+ }
+
+ # If Spanish, override with Spanish variants
+ if language == 'es':
+ spanish_overrides = {
+ 'banner_default': f"{lms_root}/static/images/enrollment_email/banner_default.png", # Same for both languages
+ 'arrow_icon_es': f"{lms_root}/static/images/enrollment_email/arrow_icon_es.png",
+ 'timer_icon_es': f"{lms_root}/static/images/enrollment_email/timer_icon_es.png",
+ 'person_icon_es': f"{lms_root}/static/images/enrollment_email/person_icon_es.png",
+ 'dollar_icon_es': f"{lms_root}/static/images/enrollment_email/dollar_icon_es.png",
+ 'flag_icon_white_es': f"{lms_root}/static/images/enrollment_email/flag_icon_white_es.png",
+ 'flag_icon_grey_es': f"{lms_root}/static/images/enrollment_email/flag_icon_grey_es.png",
+ 'flag_icon_black_en_es': f"{lms_root}/static/images/enrollment_email/flag_icon_black_en_es.png", # Same for both
+ 'calendar_icon_es': f"{lms_root}/static/images/enrollment_email/calendar_icon_es.png",
+ 'community_icon_es': f"{lms_root}/static/images/enrollment_email/community_icon_es.png",
+ 'slant_line_es': f"{lms_root}/static/images/enrollment_email/slant_line_es.png",
+ }
+ image_urls.update(spanish_overrides)
+
+ return image_urls
+
+
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
def send_course_enrollment_email(
@@ -64,13 +136,16 @@ def send_course_enrollment_email(
"course_price": CourseMode.min_course_price_for_currency(
course_id=course_id, currency="USD"
),
+ # Strip trailing slashes so template concatenation like
+ # "{{ lms_base_url }}/courses/..." never produces a double slash.
"lms_base_url": configuration_helpers.get_value(
"LMS_ROOT_URL", settings.LMS_ROOT_URL
- ),
+ ).rstrip('/'),
"learning_base_url": configuration_helpers.get_value(
"LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL
- ),
- "track_mode": track_mode
+ ).rstrip('/'),
+ "track_mode": track_mode,
+ "current_year": datetime.now().year,
}
try:
@@ -86,9 +161,14 @@ def send_course_enrollment_email(
{
"course_date_blocks": course_date_blocks,
"goals_enabled": ENABLE_COURSE_GOALS.is_enabled(course_key),
+ "user_name": user.get_full_name() or user.first_name or user.username,
}
)
+ # Inject absolute image URLs for SES rendering. Braze ignores these extra
+ # keys; they're consumed only when the SES path is enabled.
+ canvas_entry_properties.update(_build_enrollment_email_image_urls(language='en'))
+
try:
course_uuid = get_course_uuid_for_course(course_id)
if course_uuid is None:
diff --git a/common/djangoapps/student/templates/emails/enrollment_en.html b/common/djangoapps/student/templates/emails/enrollment_en.html
new file mode 100644
index 000000000000..5a637c5e8198
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/enrollment_en.html
@@ -0,0 +1,1745 @@
+
+
+
+
+
+
+
+ edX
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "emails/partials/header_en.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if banner_image_url %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% if partner_image_url %}
+
+
+
+ {% endif %}
+
+
+
+
{{ course_title }}
+
{{ org_name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if instructors %}
+
+
+
+
+
+
+
+
+
+
+ {% if instructors|length == 1 %}
+ Meet your instructor:
+ {% else %}
+ Meet your instructors:
+ {% endif %}
+
+
+
+
+ {% if not instructors|length|divisibleby:2 %}
+ {% with instructors.0 as first_instructor %}
+
+
+
+
+
+
+
+
+
+ {% for instructor in instructors %}
+ {% if not forloop.first %}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+ {% endwith %}
+
+ {% else %}
+
+
+
+
+
+
+
+
+ {% for instructor in instructors %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+ {% if goals_enabled %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Set your learning goal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Setting a goal helps you find the motivation to keep up with coursework and
+ make it to the finish line. If we notice you're not quite at your goal,
+ we'll send you an email reminder. Set your goal today.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Stay on track with your personalized course schedule
+
+
+
+
+
+ Track important dates, assignments, and more.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if course_date_blocks|length > 0 and course_date_blocks.0.due_date %}
+
+
+ Past due
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ course_date_blocks.0.due_date }}
+
+
+
+
+
+ {{ course_date_blocks.0.title }}
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+ Today
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if course_date_blocks|length > 1 %}
+ {{ course_date_blocks.1.due_date }}
+ {% endif %}
+
+
+
+
+
+ {% if course_date_blocks|length > 1 and course_date_blocks.1.title %}
+ {{ course_date_blocks.1.title }}
+ {% else %}
+ No assignments due today
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Due next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if course_date_blocks|length > 2 and course_date_blocks.2.title %}
+ {{ course_date_blocks.2.due_date }}
+ {% endif %}
+
+
+
+
+
+ {% if course_date_blocks|length > 2 and course_date_blocks.2.title %}
+ {{ course_date_blocks.2.title }}
+ {% else %}
+ No upcoming assignments
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Engage with a global community
+
+
+
+
+
+
+
+
+
+ Meet the
+
+
+
+ {% if learners_count %}{{ learners_count }}{% else %}fellow{% endif %}
+
+
+
+ learners in your course
+
+
+
+
+
+
+
+
+
+
+ Join the discussion
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "emails/partials/footer_en.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/djangoapps/student/templates/emails/enrollment_es.html b/common/djangoapps/student/templates/emails/enrollment_es.html
new file mode 100644
index 000000000000..063a1cfa1a67
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/enrollment_es.html
@@ -0,0 +1,1393 @@
+
+
+
+
+
+
+
+ edX
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "emails/partials/header_es.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Te has inscrito,
+
+
+ {{ user_name|default:"Restless Learner" }}!
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if banner_image_url %}
+
+
+
+ {% if partner_image_url %}
+
+
+
+ {% endif %}
+
+
+ {% else %}
+
+
+ {% if partner_image_url %}
+
+
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+ {{course_title}}
+
+
+
+
+
+
+
+
+
+ {{short_description}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Empieza mi curso
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if weeks_to_complete %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Se estima tardar {{weeks_to_complete}} semanas
+
+
+
+
+
+
+ {% if min_effort and max_effort %}
+ {{min_effort}} - {{max_effort}} horas por semana
+ {% elif min_effort %}
+ {% if min_effort > 1 %}
+ {{min_effort}} horas por semana
+ {% else %}
+ {{min_effort}} hora por semana
+ {% endif %}
+ {% elif max_effort %}
+ {% if max_effort > 1 %}
+ {{max_effort}} horas por semana
+ {% else %}
+ {{max_effort}} hora por semana
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if pacing_type == 'instructor_paced' %}
+ Al ritmo del profesor
+ {% else %}
+ Al ritmo del estudiante
+ {% endif %}
+
+
+
+
+
+
+ {% if pacing_type == 'instructor_paced' %}
+ Sigue un horario curricular
+ {% else %}
+ Avanza a tu propio ritmo
+ {% endif %}
+
+
+
+
+
+
+
+
+ {% if track_mode != 'masters' %}
+
+
+
+
+
+
+
+
+ {% if course_price == 0 %}
+
+
+
+
+ Gratuito
+
+
+
+
+
+
+ Opción de mejorar tu subscripción
+
+
+
+
+ {% else %}
+
+
+
+
+ ${{course_price}}
+
+
+
+
+ {% endif %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% if goals_enabled %}
+
+ {% endif %}
+
+
+
+
+
+ {% if instructors %}
+
+
+
+
+
+
+
+
+
+
+ {% if not instructors|length|divisibleby:2 %}
+ {% with instructors.0 as first_instructor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{first_instructor.name}}
+
+
+
+
+
+ {{first_instructor.organization_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for instructor in instructors %}
+ {% if not forloop.first %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{instructor.name}}
+
+
+
+
+
+ {{instructor.organization_name}}
+
+
+
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+ {% endwith %}
+ {% else %}
+
+
+
+
+
+ {% for instructor in instructors %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{instructor.name}}
+
+
+
+
+
+ {{instructor.organization_name}}
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+
+
+ {% if course_date_blocks %}
+
+
+
+
+
+
+
+
+
+
+ Mantiene un buen ritmo
+
+
+
+
+ con tu horario de curso personalizado
+
+
+
+
+
+ Recibe recordatorios sobre fechas importantes, evaluaciones y más!
+
+
+
+
+
+
+
+ {% for date in course_date_blocks %}
+ {% if forloop.first %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{date.due_date}}
+
+
+
+
+
+
+ {% if not date.assignment_type %}
+
+ {{date.title}}
+
+ {% else %}
+
+ Hora límite de entrega de
+ {{date.assignment_type}}:
+ {{date.title}}
+ a las {{date.due_time}}
+ {% if date.assignment_count == 0 %}
+ {% else %}
+ +{{date.assignment_count}}
+ más
+ {% endif %}
+
+ {% endif %}
+
+ {% elif not forloop.last %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{date.due_date}}
+
+ Hoy
+
+
+
+
+
+ {% if not date.assignment_type %}
+
+ {{date.title}}
+
+ {% else %}
+
+ Hora límite de entrega de
+ {{date.assignment_type}}:
+ {{date.title}}
+ a las {{date.due_time}}
+ {% if date.assignment_count == 0 %}
+ {% else %}
+ +{{date.assignment_count}}
+ más
+ {% endif %}
+
+ {% endif %}
+
+ {% else %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{date.due_date}}
+
+ Próxima entrega
+
+
+
+
+
+
+ {% if not date.assignment_type %}
+
+ {{date.title}}
+
+ {% else %}
+
+ Hora límite de entrega de {{date.assignment_type}}:
+ {{date.title}}
+ a las {{date.due_time}}
+ {% if date.assignment_count == 0 %}
+ {% else %}
+ +{{date.assignment_count}}
+ más
+ {% endif %}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ver mi horario
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "emails/partials/footer_es.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/djangoapps/student/templates/emails/partials/footer_en.html b/common/djangoapps/student/templates/emails/partials/footer_en.html
new file mode 100644
index 000000000000..ccf8514b984b
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/partials/footer_en.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ edX.org where learning moments become career milestones.
+
+ Need enterprise learning solutions? edX For Business
+
+ © {{ current_year }} edX LLC All rights reserved.
+
+ Update Your Preferences / Unsubscribe
+
+ 2345 Crystal Drive, Suite 1100, Arlington, VA 22202
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/common/djangoapps/student/templates/emails/partials/footer_es.html b/common/djangoapps/student/templates/emails/partials/footer_es.html
new file mode 100644
index 000000000000..e71ca66702ae
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/partials/footer_es.html
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ edX es la plataforma de confianza para la educación y el aprendizaje.
+
+ edX Para Negocios — Soluciones de aprendizaje en línea para tu organización.
+ © {{ current_year }} edX LLC Todos los derechos reservados.
+
+ Actualiza tus preferencias / Salir de la lista
+
+ 2345 Crystal Drive, Suite 1100, Arlington, VA 22202
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/common/djangoapps/student/templates/emails/partials/header_en.html b/common/djangoapps/student/templates/emails/partials/header_en.html
new file mode 100644
index 000000000000..6ed45b4f026b
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/partials/header_en.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/djangoapps/student/templates/emails/partials/header_es.html b/common/djangoapps/student/templates/emails/partials/header_es.html
new file mode 100644
index 000000000000..e6bfb5a6d7d4
--- /dev/null
+++ b/common/djangoapps/student/templates/emails/partials/header_es.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lms/static/images/enrollment_email/arrow_icon_es.png b/lms/static/images/enrollment_email/arrow_icon_es.png
new file mode 100644
index 000000000000..2285352a394e
Binary files /dev/null and b/lms/static/images/enrollment_email/arrow_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/banner_default.png b/lms/static/images/enrollment_email/banner_default.png
new file mode 100644
index 000000000000..0535b905ceb4
Binary files /dev/null and b/lms/static/images/enrollment_email/banner_default.png differ
diff --git a/lms/static/images/enrollment_email/calendar_icon_es.png b/lms/static/images/enrollment_email/calendar_icon_es.png
new file mode 100644
index 000000000000..730ae971efbc
Binary files /dev/null and b/lms/static/images/enrollment_email/calendar_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/community_icon_es.png b/lms/static/images/enrollment_email/community_icon_es.png
new file mode 100644
index 000000000000..66006cfd5d93
Binary files /dev/null and b/lms/static/images/enrollment_email/community_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/community_illustration_en.png b/lms/static/images/enrollment_email/community_illustration_en.png
new file mode 100644
index 000000000000..55140a8ef241
Binary files /dev/null and b/lms/static/images/enrollment_email/community_illustration_en.png differ
diff --git a/lms/static/images/enrollment_email/dollar_icon_en.png b/lms/static/images/enrollment_email/dollar_icon_en.png
new file mode 100644
index 000000000000..ebf7aa560829
Binary files /dev/null and b/lms/static/images/enrollment_email/dollar_icon_en.png differ
diff --git a/lms/static/images/enrollment_email/dollar_icon_es.png b/lms/static/images/enrollment_email/dollar_icon_es.png
new file mode 100644
index 000000000000..e406434837f2
Binary files /dev/null and b/lms/static/images/enrollment_email/dollar_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/flag_icon_black_en_es.png b/lms/static/images/enrollment_email/flag_icon_black_en_es.png
new file mode 100644
index 000000000000..b5fc97ee32bc
Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_black_en_es.png differ
diff --git a/lms/static/images/enrollment_email/flag_icon_grey_es.png b/lms/static/images/enrollment_email/flag_icon_grey_es.png
new file mode 100644
index 000000000000..9eff18dfc779
Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_grey_es.png differ
diff --git a/lms/static/images/enrollment_email/flag_icon_orange_en.png b/lms/static/images/enrollment_email/flag_icon_orange_en.png
new file mode 100644
index 000000000000..574e11d8f6cc
Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_orange_en.png differ
diff --git a/lms/static/images/enrollment_email/flag_icon_pink_en.png b/lms/static/images/enrollment_email/flag_icon_pink_en.png
new file mode 100644
index 000000000000..86b46ef98021
Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_pink_en.png differ
diff --git a/lms/static/images/enrollment_email/flag_icon_white_es.png b/lms/static/images/enrollment_email/flag_icon_white_es.png
new file mode 100644
index 000000000000..9bc8536f4c56
Binary files /dev/null and b/lms/static/images/enrollment_email/flag_icon_white_es.png differ
diff --git a/lms/static/images/enrollment_email/goal_idea_icon_en.png b/lms/static/images/enrollment_email/goal_idea_icon_en.png
new file mode 100644
index 000000000000..0d9c55ad7031
Binary files /dev/null and b/lms/static/images/enrollment_email/goal_idea_icon_en.png differ
diff --git a/lms/static/images/enrollment_email/logo_url.png b/lms/static/images/enrollment_email/logo_url.png
new file mode 100644
index 000000000000..419f27df2c3e
Binary files /dev/null and b/lms/static/images/enrollment_email/logo_url.png differ
diff --git a/lms/static/images/enrollment_email/person_icon_en.png b/lms/static/images/enrollment_email/person_icon_en.png
new file mode 100644
index 000000000000..a05d7bcfd447
Binary files /dev/null and b/lms/static/images/enrollment_email/person_icon_en.png differ
diff --git a/lms/static/images/enrollment_email/person_icon_es.png b/lms/static/images/enrollment_email/person_icon_es.png
new file mode 100644
index 000000000000..1f79fee7028f
Binary files /dev/null and b/lms/static/images/enrollment_email/person_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/slant_line_es.png b/lms/static/images/enrollment_email/slant_line_es.png
new file mode 100644
index 000000000000..ce4e037273e3
Binary files /dev/null and b/lms/static/images/enrollment_email/slant_line_es.png differ
diff --git a/lms/static/images/enrollment_email/timer_icon_en.png b/lms/static/images/enrollment_email/timer_icon_en.png
new file mode 100644
index 000000000000..ca3b690b2d61
Binary files /dev/null and b/lms/static/images/enrollment_email/timer_icon_en.png differ
diff --git a/lms/static/images/enrollment_email/timer_icon_es.png b/lms/static/images/enrollment_email/timer_icon_es.png
new file mode 100644
index 000000000000..55f4b54ce789
Binary files /dev/null and b/lms/static/images/enrollment_email/timer_icon_es.png differ
diff --git a/lms/static/images/enrollment_email/vertical_line_black_en.png b/lms/static/images/enrollment_email/vertical_line_black_en.png
new file mode 100644
index 000000000000..191d1bf257ba
Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_black_en.png differ
diff --git a/lms/static/images/enrollment_email/vertical_line_orange_en.png b/lms/static/images/enrollment_email/vertical_line_orange_en.png
new file mode 100644
index 000000000000..459f1fe1606d
Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_orange_en.png differ
diff --git a/lms/static/images/enrollment_email/vertical_line_white_en.png b/lms/static/images/enrollment_email/vertical_line_white_en.png
new file mode 100644
index 000000000000..96f66d5ba919
Binary files /dev/null and b/lms/static/images/enrollment_email/vertical_line_white_en.png differ
diff --git a/lms/static/images/enrollment_email/you_are_enrolled_en.png b/lms/static/images/enrollment_email/you_are_enrolled_en.png
new file mode 100644
index 000000000000..e7282f071085
Binary files /dev/null and b/lms/static/images/enrollment_email/you_are_enrolled_en.png differ
diff --git a/openedx/core/djangoapps/ace_common/utils.py b/openedx/core/djangoapps/ace_common/utils.py
index 7cf38c821976..e84bc2230f7d 100644
--- a/openedx/core/djangoapps/ace_common/utils.py
+++ b/openedx/core/djangoapps/ace_common/utils.py
@@ -1,9 +1,6 @@
"""
Utility functions for edx-ace.
"""
-import logging
-
-log = logging.getLogger(__name__)
def setup_firebase_app(firebase_credentials, app_name='fcm-app'):
diff --git a/openedx/core/djangoapps/user_authn/tasks.py b/openedx/core/djangoapps/user_authn/tasks.py
index c1c781d2f26f..3c425a1de6f4 100644
--- a/openedx/core/djangoapps/user_authn/tasks.py
+++ b/openedx/core/djangoapps/user_authn/tasks.py
@@ -18,9 +18,24 @@
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.utils import check_pwned_password
from openedx.core.lib.celery.task_utils import emulate_http_request
+from edx_toggles.toggles import WaffleFlag
log = logging.getLogger('edx.celery.task')
+# .. toggle_name: user_authn.enable_ses_for_account_activation
+# .. toggle_implementation: WaffleFlag
+# .. toggle_default: False
+# .. toggle_description: Route account activation emails via SES using ACE.
+# .. toggle_use_cases: opt_in, temporary
+# .. toggle_creation_date: 2026-03-31
+# .. toggle_target_removal_date: None
+# .. toggle_warning: Controls SES routing for account activation emails.
+
+ENABLE_SES_FOR_ACCOUNT_ACTIVATION = WaffleFlag(
+ 'user_authn.enable_ses_for_account_activation',
+ __name__,
+)
+
@shared_task
@set_code_owner_attribute
@@ -60,6 +75,9 @@ def send_activation_email(self, msg_string, from_address=None, site_id=None):
max_retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
retries = self.request.retries
+ if msg.options is None:
+ msg.options = {}
+
if from_address is None:
from_address = configuration_helpers.get_value('ACTIVATION_EMAIL_FROM_ADDRESS') or (
configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
@@ -71,28 +89,63 @@ def send_activation_email(self, msg_string, from_address=None, site_id=None):
site = Site.objects.get(id=site_id) if site_id else Site.objects.get_current()
user = User.objects.get(id=msg.recipient.lms_user_id)
+ route_via_ses = ENABLE_SES_FOR_ACCOUNT_ACTIVATION.is_enabled()
+ sent_via_ses = False
+
+ if route_via_ses:
+ msg.options['override_default_channel'] = 'django_email'
+
try:
with emulate_http_request(site=site, user=user):
ace.send(msg)
+ sent_via_ses = route_via_ses
+
except RecoverableChannelDeliveryError:
- log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
- dest_addr=dest_addr,
- attempt=retries,
- max_attempts=max_retries
- ))
- try:
- self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries)
- except MaxRetriesExceededError:
- log.error(
- 'Unable to send activation email to user from "%s" to "%s"',
- from_address,
+ if route_via_ses:
+ log.warning(
+ "SES send failed for %s, falling back to default ACE channel",
dest_addr,
- exc_info=True
+ exc_info=True,
)
+
+ msg.options.pop('override_default_channel', None)
+
+ with emulate_http_request(site=site, user=user):
+ ace.send(msg)
+ sent_via_ses = False
+
+ else:
+ log.info(
+ 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
+ dest_addr=dest_addr,
+ attempt=retries,
+ max_attempts=max_retries
+ )
+ )
+ try:
+ self.retry(
+ countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT,
+ max_retries=max_retries
+ )
+ except MaxRetriesExceededError:
+ log.error(
+ 'Unable to send activation email to user from "%s" to "%s"',
+ from_address,
+ dest_addr,
+ exc_info=True
+ )
+ return
+
except Exception:
log.exception(
'Unable to send activation email to user from "%s" to "%s"',
from_address,
dest_addr,
)
- raise Exception # lint-amnesty, pylint: disable=raise-missing-from
+ raise
+
+ log.info(
+ 'Activation email for %s sent via %s',
+ dest_addr,
+ 'SES' if sent_via_ses else 'default ACE channel',
+ )