Skip to content

feat: add SES routing for account activation emails with fallback support#207

Draft
nthriveni-sonata-ship-it wants to merge 2 commits intoedx:release-ulmofrom
nthriveni-sonata-ship-it:feature/account-activation-ses-routing
Draft

feat: add SES routing for account activation emails with fallback support#207
nthriveni-sonata-ship-it wants to merge 2 commits intoedx:release-ulmofrom
nthriveni-sonata-ship-it:feature/account-activation-ses-routing

Conversation

@nthriveni-sonata-ship-it
Copy link
Copy Markdown
Member

Summary

Adds feature-flagged SES routing for account activation emails with a safe fallback mechanism.

Changes

  • Centralized SES routing logic in ace_common/utils.py
  • Removed SES logic from management view
  • Added fallback handling in send_activation_email task
  • Introduced WaffleFlag: user_authn.enable_ses_for_account_activation

Behavior

  • Flag OFF → default ACE flow
  • Flag ON → SES routing via django_email
  • If SES fails → fallback to default ACE channel

Testing

  • Verified default flow with flag OFF
  • Verified SES routing with flag ON
  • Simulated SES failure and confirmed fallback to default ACE channel

@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/account-activation-ses-routing branch from 4aeff43 to d52602d Compare April 2, 2026 06:17
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/account-activation-ses-routing branch from d52602d to 83ef742 Compare April 2, 2026 06:58
Comment thread openedx/core/djangoapps/user_authn/tasks.py
)
return

_remove_ses_overrides(msg)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback happens in the except RecoverableChannelDeliveryError block.

Specifically:

  • We remove SES overrides via _remove_ses_overrides(msg)
  • Then call ace.send(msg) again

That second ace.send(msg) routes via the default ACE channel (Braze).

Comment thread openedx/core/djangoapps/user_authn/tasks.py
msg.options.update({
'transactional': True,
'override_default_channel': 'django_email',
'from_address': configuration_helpers.get_value(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking in the management.py file, it looks like this action is already handled here

"""
Apply SES routing to ACE message if flag is enabled.
"""
if not ENABLE_SES_FOR_ACCOUNT_ACTIVATION.is_enabled():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this probably doesn't belong in the ace_common utils file, since if we look at Goal Reminder's implementation it lives closer to the management command.

Comment thread openedx/core/djangoapps/user_authn/tasks.py
Comment on lines +99 to +103
log.warning(
"SES send failed for %s, falling back to default ACE channel",
dest_addr,
exc_info=True,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird one, because we'd have to remove this log eventually if we either transition fully to AWS SES, right? I think we can maybe wrap this in an if route_via_ses condition?

def _remove_ses_overrides(msg):
msg.options.pop('override_default_channel', None)
msg.options.pop('transactional', None)
msg.options.pop('from_address', None)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming that we don't need to add the from_address, as it has all been the same, do we want to pop this?

)


def _remove_ses_overrides(msg):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last comment: I feel like the entire block of the first RecoverableChannelDeliveryError doesn't need to be changed much and that you don't need to add the same logic for RecoverableChannelDeliveryError again.
To simplify the logic here, I think this method and the log.warning that announces the ACE fallback will be used can both be wrapped in an if routing_via_ses condition at the top of the RecoverableChannelDeliveryError block and everything after can remain untouched (aside from the log.info you added about whether SES or "default ACE channel" was used.

msg.options = {}

msg.options.update({
'transactional': True,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccountActivation in the management.py also sets the transactional property to True, so I don't think this is necessary. If anything, this further brings up the point that most of this can be moved to the management.py file and not live in ace_common.

@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/account-activation-ses-routing branch 7 times, most recently from 00851e2 to af0e027 Compare April 16, 2026 04:58
@nthriveni-sonata-ship-it nthriveni-sonata-ship-it force-pushed the feature/account-activation-ses-routing branch from af0e027 to 3917337 Compare April 16, 2026 04:58
"""
import logging


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can probably remove this line and that means the only changes made are in tasks.py

Copy link
Copy Markdown
Member

@jsnwesson jsnwesson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from the one new comment i made, everything looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants