Opened 3 weeks ago

Closed 2 weeks ago

Last modified 2 weeks ago

#37443 closed New feature (needsnewfeatureprocess)

Add support for email delivery using Django's Tasks framework.

Reported by: David Smith Owned by: mudassar
Component: Core (Mail) Version: 6.1
Severity: Normal Keywords:
Cc: Jake Howard, Mike Edmunds Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The PasswordResetView ​docs includes a warning that sending emails during a request can expose an email address enumeration timing attack. The recommendation is to send emails asynchronously with a link to ​django-mailer. At the time of writing django-mailer last saw a release on PyPI in 2024 (~2.5 years ago).

Given that Django now has an inbuilt ​tasks framework I propose that we use this to provide built-in integration for sending emails in the background.

Very broadly, Django would provide a way to opt-in to sending emails using django.tasks. It would be possible to create a task which sends an email using one of the configured ​mailers. This would allow existing code which sends emails, including the password reset view, to use the tasks framework to send emails. Production setups with a django tasks backend with a worker process would then be able to send emails in the background.

The django tasks framework would seem suited to this, with the first example in the docs being a task that sends an email. See ​docs.

Change History (9)

comment:1 by Natalia Bidart, 3 weeks ago

Severity: Low → Normal
Triage Stage: Unreviewed → Accepted

Thanks David for the ticket! I agree that Django should offer something first-party here, since our own docs currently use sending email as the main example for tasks but then point to a third-party package in the PasswordResetView warning.

That said, I'm not super sure if we should change PasswordResetForm (or any other built-in caller) to use tasks directly. The timing concern applies to any view that sends emails conditionally, and how emails get delivered is a project-level decision, not something each call site should own (nor decide on).

What I'd like to see instead is a super easy and well documented way to switch email delivery to the background, via tasks or anything else, without touching the code that calls send_mail() or EmailMessage.send(). With MAILERS in place, I think an email backend that enqueues a task, which then delivers through another configured mailer, is the natural fit, and it would cover the password reset flow and third-party apps alike. One thing to sort out early: task arguments must be JSON-serializable, so the message would need to be serialized rather than passed as an EmailMessage instance.

Whatever the final shape, the docs should show the full setup, and the PasswordResetView warning should point there instead of to django-mailer

comment:2 by mudassar, 3 weeks ago

Owner: set to mudassar
Status: new → assigned

comment:3 by Jake Howard, 2 weeks ago

Cc: Jake Howard added

What I'd like to see instead is a super easy and well documented way to switch email delivery to the background, via tasks or anything else, without touching the code that calls send_mail() or EmailMessage.send().

When I originally implemented django-tasks, it shipped with a drop-in mail backend to do this. However, as part of the ​SC vote for DEP 14, Adam flagged that it's not possible to make it API compliant:

Such an email backend would break the APIs of send_mail() and send_mass_mail() since they return success or failure information.

Therefore it was ​removed. If sending an email fails, it needs to be reflected in the return value, which is only possible if the API waits for tasks, which defeats the name benefit.

I think the only solution to this would be a new API, or a change to the existing API contract. Or, expand the task and push towards building the email and context into it.

comment:4 by mudassar, 2 weeks ago

Has patch: set

comment:6 by Jake Howard, 2 weeks ago

Patch needs improvement: set

comment:7 by Mike Edmunds, 2 weeks ago

Cc: Mike Edmunds added

comment:8 by mudassar, 2 weeks ago

Patch needs improvement: unset

comment:9 by Mike Edmunds, 2 weeks ago

Resolution: → needsnewfeatureprocess
Status: assigned → closed

I think a task-based EmailBackend is exactly the right idea. And I also think it needs to go through the new features process to refine the design and behavior. (And very likely will may need to be prototyped as a third-party package.)

Mudassar, your PR and Jake Howard's review were helpful to identify some of the considerations that would need to be addressed in the feature design. (So thank you for for those.)

A handful of questions off the top of my head:

  • How should a Django message object be serialized for the task queue?
  • How do we accommodate third-party packages that add message attributes. (E.g., django-celery-email-reboot has a CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES setting that specifies additional attrs to include in json serialization.)
  • How does the send_email task distinguish retryable failures from permanent ones? Can users customize this?
  • How are permanent failures handled?

In general, we can probably adopt some ideas from django-mailer and django-celery-email[-reboot] capabilities (and outstanding issues).

Again, though, this feels significant enough that it should be refined in the new features process rather than in a PR or ticket. I'm updating the status to reflect that.

Last edited 2 weeks ago by Mike Edmunds (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top