HTML formatted emails get sent twice (with and without format)

Hi, I’d appreciate some feedback on this issue. I am trying to set the forgot password appflow from django and to keep the look and feel of my webapp I customized the emails with HTML format.

I’ve already did this with user_creation confirmation email which worked just fine. THinking this would be the same kind of problem I followed the django docs but when the user fills the form with the email, and the email was found in the db, two emails are being sent automatically (the plain text one and the formatted one).

The code in the user_creation and the password retrieval process is almost identical, but still one works and the other one doesn’t.

Here’s the code that’s having troubles…

class CustomPasswordResetView(PasswordResetView):
    template_name = 'usuarios/password/password_reset_form.html'
    success_url = reverse_lazy('usuarios:password_reset_done')
    subject_template_name = 'emails/password_reset_subject.txt'
    email_template_name = 'emails/password_reset_email.html'
    from_email = settings.DEFAULT_FROM_EMAIL

    def send_mail(self, *args, **kwargs):
        pass

    def form_valid(self, form):
        frint("Iniciando envío de correo de recuperación") #frint is a print flushed decorated fx
        
        users = list(form.get_users(form.cleaned_data['email']))
        if not users:
            form.add_error(None, "No se encontró ningún usuario con ese correo electrónico.")
            return super().form_invalid(form)

        user = users[0]

        uidb64 = urlsafe_base64_encode(force_bytes(user.pk))
        token = default_token_generator.make_token(user)
        
        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': self.request.get_host(),
            'uid': uidb64,
            'token': token,
        }

        email_subject = 'Recuperación de contraseña'
        email_body = render_to_string('emails/password_reset_email.html', context)
        plain_message = strip_tags(email_body)  # Generar mensaje de texto sin formato

        email = EmailMultiAlternatives(
            subject=email_subject,
            body=plain_message,
            from_email=self.from_email,
            to=[form.cleaned_data['email']],
        )
        email.attach_alternative(email_body, "text/html")
        
        try:
            email.send()
            frint("Correo de recuperación enviado correctamente")
        except Exception as e:
            frint(f"Error al enviar el correo: {str(e)}")

        return super().form_valid(form)

I tried passing the send email method but nothing happened… the only thing I did that made a difference was removing “email.attach_alternative(email_body, “text/html”)” which obviously broke the mail content, but! only one blank email was sent…

any clues? Thanks in advance!

Ok so, I got over this and I will leave my own reply in case someone is dealing with this problem.
The magic of it was drilling through the class.
I went deep into the django built in classes until I reached the PasswordResetView Form.
In there I’ve realized that this class also uses EmailMultiAlternatives, so there was no need to call this function in a customized manner.

the contextMixin adds some extra options to add context so I did extra_email_context = …
and another class attr was needed: html_email_template. because to trigger the EmailMultiAlternatives that argument has to be declared.

This is the built-in form

class PasswordResetForm(forms.Form):
    email = forms.EmailField(
        label=_("Email"),
        max_length=254,
        widget=forms.EmailInput(attrs={"autocomplete": "email"}),
    )

    def send_mail(
        self,
        subject_template_name,
        email_template_name,
        context,
        from_email,
        to_email,
        html_email_template_name=None,
    ):
        """
        Send a django.core.mail.EmailMultiAlternatives to `to_email`.
        """
        subject = loader.render_to_string(subject_template_name, context)
        # Email subject *must not* contain newlines
        subject = "".join(subject.splitlines())
        body = loader.render_to_string(email_template_name, context)

        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
        if html_email_template_name is not None:
            html_email = loader.render_to_string(html_email_template_name, context)
            email_message.attach_alternative(html_email, "text/html")

        email_message.send()

So logically I abandoned my code and went with this… adding the necessary attrs.
And it felt gooood!

So if you are struggling with this. just dig through django backend documents. Its pretty straight forward (btw, chat gpt wasn’t able to figure it out, *faith in humanity)