Skip to content

Commit ab7847f

Browse files
feat: route account activation emails via SES using waffle flag
1 parent 4a49a95 commit ab7847f

1 file changed

Lines changed: 81 additions & 28 deletions

File tree

  • openedx/core/djangoapps/user_authn
Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
11
"""
22
This file contains celery tasks for sending email
33
"""
4-
4+
55
import logging
6-
6+
77
from celery import shared_task
88
from celery.exceptions import MaxRetriesExceededError
99
from 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
1111
from django.contrib.sites.models import Site
1212
from edx_ace import ace
1313
from edx_ace.errors import RecoverableChannelDeliveryError
1414
from edx_ace.message import Message
1515
from edx_django_utils.monitoring import set_code_owner_attribute
16-
16+
1717
from common.djangoapps.track import segment
1818
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
1919
from openedx.core.djangoapps.user_authn.utils import check_pwned_password
2020
from openedx.core.lib.celery.task_utils import emulate_http_request
21-
21+
from edx_toggles.toggles import WaffleFlag
22+
2223
log = 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
2742
def 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
5469
def 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

Comments
 (0)