11"""
22This file contains celery tasks for sending email
33"""
4-
4+
55import logging
6-
6+
77from celery import shared_task
88from celery .exceptions import MaxRetriesExceededError
99from django .conf import settings
10- from django .contrib .auth .models import User # pylint: disable=imported-auth-user
10+ from django .contrib .auth .models import User # lint-amnesty, pylint: disable=imported-auth-user
1111from django .contrib .sites .models import Site
1212from edx_ace import ace
1313from edx_ace .errors import RecoverableChannelDeliveryError
1414from edx_ace .message import Message
1515from edx_django_utils .monitoring import set_code_owner_attribute
16-
16+
1717from common .djangoapps .track import segment
1818from openedx .core .djangoapps .site_configuration import helpers as configuration_helpers
1919from openedx .core .djangoapps .user_authn .utils import check_pwned_password
2020from openedx .core .lib .celery .task_utils import emulate_http_request
21-
21+ from edx_toggles .toggles import WaffleFlag
22+
2223log = logging .getLogger ('edx.celery.task' )
23-
24-
24+
25+ # .. toggle_name: user_authn.enable_ses_for_account_activation
26+ # .. toggle_implementation: WaffleFlag
27+ # .. toggle_default: False
28+ # .. toggle_description: Route account activation emails via SES using ACE.
29+ # .. toggle_use_cases: opt_in, temporary
30+ # .. toggle_creation_date: 2026-03-31
31+ # .. toggle_target_removal_date: None
32+ # .. toggle_warning: Controls SES routing for account activation emails.
33+
34+ ENABLE_SES_FOR_ACCOUNT_ACTIVATION = WaffleFlag (
35+ 'user_authn.enable_ses_for_account_activation' ,
36+ __name__ ,
37+ )
38+
39+
2540@shared_task
2641@set_code_owner_attribute
2742def check_pwned_password_and_send_track_event (
@@ -46,53 +61,91 @@ def check_pwned_password_and_send_track_event(
4661 'Unable to get response from pwned password api for user_id: "%s"' ,
4762 user_id ,
4863 )
49- return {} # pylint: disable=raise-missing-from
50-
51-
64+ return {} # lint-amnesty, pylint: disable=raise-missing-from
65+
66+
5267@shared_task (bind = True , default_retry_delay = 30 , max_retries = 2 )
5368@set_code_owner_attribute
5469def send_activation_email (self , msg_string , from_address = None , site_id = None ):
5570 """
5671 Sending an activation email to the user.
5772 """
5873 msg = Message .from_string (msg_string )
59-
74+
6075 max_retries = settings .RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
6176 retries = self .request .retries
62-
77+
78+ if msg .options is None :
79+ msg .options = {}
80+
6381 if from_address is None :
6482 from_address = configuration_helpers .get_value ('ACTIVATION_EMAIL_FROM_ADDRESS' ) or (
6583 configuration_helpers .get_value ('email_from_address' , settings .DEFAULT_FROM_EMAIL )
6684 )
6785 msg .options ['from_address' ] = from_address
68-
86+
6987 dest_addr = msg .recipient .email_address
70-
88+
7189 site = Site .objects .get (id = site_id ) if site_id else Site .objects .get_current ()
7290 user = User .objects .get (id = msg .recipient .lms_user_id )
73-
91+
92+ route_via_ses = ENABLE_SES_FOR_ACCOUNT_ACTIVATION .is_enabled ()
93+ sent_via_ses = False
94+
95+ if route_via_ses :
96+ msg .options ['override_default_channel' ] = 'django_email'
97+
7498 try :
7599 with emulate_http_request (site = site , user = user ):
76100 ace .send (msg )
101+ sent_via_ses = route_via_ses
102+
77103 except RecoverableChannelDeliveryError :
78- log .info ('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}' .format ( # noqa: UP032 # pylint: disable=line-too-long
79- dest_addr = dest_addr ,
80- attempt = retries ,
81- max_attempts = max_retries
82- ))
83- try :
84- self .retry (countdown = settings .RETRY_ACTIVATION_EMAIL_TIMEOUT , max_retries = max_retries )
85- except MaxRetriesExceededError :
86- log .error (
87- 'Unable to send activation email to user from "%s" to "%s"' ,
88- from_address ,
104+ if route_via_ses :
105+ log .warning (
106+ "SES send failed for %s, falling back to default ACE channel" ,
89107 dest_addr ,
90- exc_info = True
108+ exc_info = True ,
91109 )
110+
111+ msg .options .pop ('override_default_channel' , None )
112+
113+ with emulate_http_request (site = site , user = user ):
114+ ace .send (msg )
115+ sent_via_ses = False
116+
117+ else :
118+ log .info (
119+ 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}' .format (
120+ dest_addr = dest_addr ,
121+ attempt = retries ,
122+ max_attempts = max_retries
123+ )
124+ )
125+ try :
126+ self .retry (
127+ countdown = settings .RETRY_ACTIVATION_EMAIL_TIMEOUT ,
128+ max_retries = max_retries
129+ )
130+ except MaxRetriesExceededError :
131+ log .error (
132+ 'Unable to send activation email to user from "%s" to "%s"' ,
133+ from_address ,
134+ dest_addr ,
135+ exc_info = True
136+ )
137+ return
138+
92139 except Exception :
93140 log .exception (
94141 'Unable to send activation email to user from "%s" to "%s"' ,
95142 from_address ,
96143 dest_addr ,
97144 )
98- raise Exception # pylint: disable=raise-missing-from # noqa: B904
145+ raise
146+
147+ log .info (
148+ 'Activation email for %s sent via %s' ,
149+ dest_addr ,
150+ 'SES' if sent_via_ses else 'default ACE channel' ,
151+ )
0 commit comments