Deprecating or changing how django.core.mail handles fail_silently

[EDIT: just joining the discussion? Jump down to #comment 9 for the current proposal.]

I’m proposing we rework how Django’s mail APIs handle fail_silently, and seeking feedback and consensus from the group before opening a ticket.

The proposal is to hoist fail_silently handling out of individual EmailBackend implementations, and instead wrap the two places Django calls backend.send_messages() with a broad try/except to:

  • make fail_silently more predictable, by ignoring all exceptions consistently across all backends (including message serialization and address validation errors that currently may or may not be ignored, depending on the email backend in use)
  • address an API shape problem that will conflict with the upcoming dict-based EMAIL_PROVIDERS feature (ticket-35514, draft DEP 0018)

(This came up while drafting the EMAIL_PROVIDERS DEP, and it’s currently included in there. @nessita and I thought it might be helpful to break it into a separate task and discussion.)

Background

Most Django mail APIs take a fail_silently arg meant to ignore errors during sending. One use is avoiding cascading failures in error reporting, like in calls to mail_admins() from the logging AdminEmailHandler or mail_managers() from the BrokenLinkEmailMiddleware.

Although presented and described as a modifier to the email sending process, fail_silently is currently implemented as EmailBackend configuration (a backend __init__() param). This creates three problems:

  • Individual backend implementations apply their own, inconsistent interpretations of which errors to ignore. Django’s SMTP backend ignores only smtplib.SMTPException (plus OSError, but only during connection setup and SSL handshaking). But there are also several third-party email backend packages,[1] and each has a slightly different approach to fail_silently handling.

  • If you pass both connection and fail_silently to the same mail function, Django silently ignores fail_silently and instead fails loudly :upside_down_face:. (ticket-36894. The connection is already initialized, so it’s too late to apply fail_silently.)

  • For related reasons, this conflicts with the upcoming EMAIL_PROVIDERS feature. The proposed mail.providers[alias] would return a fully configured EmailBackend instance (like similar APIs for tasks, storages, caches and db connections). Since it’s already initialized, it’s too late to change fail_silently for that provider instance.

Proposal

To address all these issues, I propose[2] moving fail_silently handling out of individual EmailBackend implementations. Instead, we’d catch all exceptions where the message is being sent.

Specifically, we’d change EmailMessage.send():

  class EmailMessage:
      def send(self, fail_silently=False):
          if not self.recipients():
              return 0
-         return self.get_connection(fail_silently).send_messages([self])
+         connection = self.get_connection()  # no fail_silently
+         try:
+             return connection.send_messages([self])
+         except Exception:
+             if fail_silently:  # see compatibility note
+                 return 0
+             raise
  • Errors in get_connection() are never ignored, either before or after the change. (These are typically settings and configuration errors from the EmailBackend constructor. Despite the name, get_connection() doesn’t actually open any connections.)
  • After the change, all errors in connection.send_messages() would be ignored with fail_silently.
  • We’d make a similar change to send_mass_mail() (which is the only other Django code that calls send_messages() directly; everything else delegates to EmailMessage.send()).
  • When we implement EMAIL_PROVIDERS, the connection = self.get_connection() line will get replaced with connection = mail.providers[using] (with a new using argument for the provider alias).

This change would take effect immediately in Django 6.1, without deprecation. (It’s arguably a bug fix or cleanup: it makes EmailMessage.send(fail_silently=True) match the docs and most callers’ expectations.)

There are two related deprecations and a compatibility shim:

  • The fail_silently argument to django.core.mail.get_connection() would be deprecated in Django 6.1 and removed in Django 7.0. (Note that the entire get_connection() function would also be deprecated as part of EMAIL_PROVIDERS.)
  • The (inconsistent) implementations of fail_silently within Django’s built-in email backends would remain in place until Django 7.0, but would effectively be dead code in most cases. Initializing a built-in backend with fail_silently=True would cause a deprecation warning. (This covers code that uses get_connection(fail_silently=True) and then calls send_messages() directly on that connection.)
  • During the deprecation period, the line marked “see compatibility note” above would also need to check getattr(connection, "fail_silently", False), for compatibility with connections created outside EmailMessage.

Impact

Here are some examples of what this proposal would and would not change, for Django’s SMTP backend and fail_silently=True:

  • Configuration and backend initialization: no change (not ignored now, not ignored after)

    • EMAIL_BACKEND points to nonexistent class: ImportError in get_connection()
    • Conflicting settings EMAIL_USE_SSL = True and EMAIL_USE_TLS = True: ValueError in smtp.EmailBackend.__init()__
  • Message serialization and address validation: :warning: CHANGE (not ignored now, would be ignored after)

    • CRLF in from_email="from@\r\nexample.com": ValueError in EmailMessage.message()
    • Invalid encoding body="\udce9": UnicodeEncodeError in EmailMessage.message()
    • (The message is not sent in either case; the only impact is whether an error propagates out of the send call)
  • SMTP connection and communication: no change (ignored now, ignored after)

    • DNS failure on EMAIL_HOST: socket.gaierror in smtp.EmailBackend.open()
    • Bad EMAIL_HOST_USER/EMAIL_HOST_PASSWORD: SMTPAuthenticationError in smtp.EmailBackend.open()
    • Connection interrupted during transmission: SMTPServerDisconnected in smtp.EmailBackend._send()
  • RuntimeError, MemoryError: :warning: CHANGE (not ignored now, would be ignored after)

For third-party email backends the list would be different. Some already ignore message serialization errors with fail_silently=True, so this proposal wouldn’t change their resulting behavior. In general, anything detected at __init__() time would still be raised; everything in send_messages() would be suppressed.

One other change of note is the django.mail.outbox used during testing. Django’s locmem email backend doesn’t implement fail_silently, so no errors are ignored when it is in use. In a test case, this code would raise an error now but won’t after the change:

mail_admins("bad\r\nsubject", "message", fail_silently=True)

Alternatives

We could leave fail_silently implemented as it is now and instead improve the docs (ticket-36907). But that creates a big problem for EMAIL_PROVIDERS. There’s more background and several proposed options in an earlier revision of DEP 0018. After discussion with @nessita, this seemed the cleanest (and it’s now the only option in the latest DEP revision).


  1. I maintain one of them. ↩︎

  2. Proper attribution (and AI disclosure): I had actually proposed several other approaches for fail_silently with EMAIL_PROVIDERS, but wasn’t happy with any of them. Claude 4.6 Opus first suggested what’s proposed here, during a technical review of an early DEP 0018 draft. ↩︎

2 Likes

I guess one other option would be to deprecate fail_silently in all mail APIs and remove it completely in Django 7.0. Users that want to ignore email errors could provide their own exception handling (and choose exactly which errors they want to be silent).

I’ve never really understood why someone would want to ignore some-but-not-all errors when emailing managers about broken links, or especially when emailing admins with error logs. I haven’t used the logging AdminEmailHandler in production in a while, but when I did, I definitely didn’t want it to silently swallow errors sending those emails.

(I originally thought fail_silently was there to avoid an infinite loop in AdminEmailHandler. It’s not. Errors in logging handlers aren’t normally logged as errors, for exactly that reason. And even if they were, to prevent loops fail_silently would need to ignore all errors, not just SMTP errors.)

Is there some use case I’m missing?

I’d like to move this forward to make room for EMAIL_PROVIDERS. From that lack of responses, I gather nobody has strong opinions one way or the other. Which isn’t surprising: I’m pretty sure fail_silently doesn’t show up much in production Django code.

Unless there’s further (human) input, I’ll open a ticket to deprecate fail_silently for everything except mail_admins() and mail_managers()—option 3 below.

I poked into fail_silently usage through GitHub code search, then asked three AIs for a broader perspective on how it’s used in projects they’ve seen. (I hoped their training data might supply the answer to my earlier question, “Is there some use case I’m missing?” The AI analyses seemed to align with patterns I had spotted in my GitHub search.)

My takeaways:

  • The overwhelming majority of django.core.mail calls use the default fail_silently=False. (It’s unusual to ignore email errors in production. This decision just doesn’t impact that much code.)
  • Many uses of fail_silently=True were likely copied from older tutorials or references, for no good reason. (Two AI’s referred to “cargo culting.”)
  • Where fail_silently=True is intentional, callers seem to have different expectations for which errors would be silenced. Many want “don’t crash this path” (ignore all errors), but others seem to be saying “don’t bother me with network glitches” and would want other errors to be loud.
  • The exception is mail_admins(): when it’s called with fail_silently=True (which is also rare), it’s in error handling paths that want to avoid cascading errors. Callers expect it to suppress all exceptions. (The same argument probably applies to mail_managers(), but that function isn’t widely used.)
  • All uses of fail_silently expect it to apply to that one send—nobody thinks of it as EmailBackend configuration.

I see three options:

  1. Make fail_silently ignore all Exceptions (broad try/catch in EmailMessage.send() as described in the first post in this thread; GPT and Claude both wanted to refine this to not ignore MemoryError or RecursionError)

  2. Deprecate fail_silently and remove it in Django 7.0. Callers would need to replace fail_silently=True with their own exception handling, forcing them to be explicit about which (if any) exceptions they want to ignore (second post in this thread)

  3. Hybrid approach: Keep fail_silently for mail_admins() and mail_managers() (ignoring all Exceptions), but deprecate and remove it for send_mail(), send_mass_mail() and EmailMessage.send().

I’ve tried to map the AIs’ opinions to our usual voting scale:

Mike (human) Claude 4.6 Opus Gemini 3.1 Pro GPT 5.3 Codex
Option 1 +0 +1 +0 +1
Option 2 -1 -0 -1 -1 -1
Option 3 +1 -0 +1 -1

Gemini and I both feel option 3 is pragmatic for real-world use, and easy to explain and document. (I think GPT’s a little alarmist about the impact of removing it, so I’m discounting its -1 on option 3.)

Full chat transcripts: * django.core.mail fail_silently usage.md · GitHub. (The options were discussed in a different order from above: 1=A, 2=C, 3=B.)

Hi Mike, a human here to talk to you. I think option 3 sounds the best way forward. It’s weird to reproduce a language feature (try/except) as an argument.

We can make Django-upgrade drop fail_silently=False from the functions it will be deprecated from, at least. That won’t help folks who set it to True but I’m imagining it will help for cases where IDES or copy pasta populated the argument unnecessarily.

1 Like

Hi Mike, you were on my list, sorry for not reaching out sooner! Your notes were super helpful, as I’m new to this area of Django.

Is there some use case I’m missing?

My impressions are:

  • fail_silently was used to rhyme with contrib.messages, which exposes this same argument for the use case of a reusable app (such as the admin!), that cannot guarantee that contrib.messages is installed. I see the use case for email as basically the same: I’m writing a reusable app: I’d like to send some mail, so long as my users have email configured at all.
  • I also see a use case for test suites.
  • I like that API. (I don’t think it’s a weird abstraction.)
  • Thanks for the ticket about the imprecise wording in the docs. But leaving the imprecision to the side, my impression is that once again, just like-fail-silently in contrib.messages only catching the error for the messages app not being installed, the email backends should only be catching connection errors. SMTP connection errors became a subclass of OSError in Python 3.4. I agree with generalizing the catch as far as OSError, but not further.
  • I’m unclear what we gain from the hybrid approach of deprecating the arg in some places and not others (and unclear how this unblocks the DEP).

So, in summary, I’d vote for option 1 with catching OSError.

What do you think?

1 Like

(I forgot to add: I understand OSError might be not workable for 3rd party backends that raise their connection errors directly from Exception, so we could fiddle with that, and possibly ship an exception that could be subclassed. This would be pointless if no 3rd party backends would be willing to make that change, so I’m eager to hear your forecast on that, Mike!) Catching all exceptions is of course still an option.

I’m not sure that the parallel with contrib.messages holds, for two reasons:

  1. IMHO the concerns are different: messages.fail_silently guards against the messages middleware not being installed/available, which could be seen as a deployment/configuration condition. Email’s fail_silently suppresses runtime errors (SMTP down, connection refused, etc) and these are not the same kind of problem, so I’m thinking that the API analogy doesn’t transfer cleanly.

  2. Perhaps more fundamentally, I think fail_silently was designed for a single-backend world, where “degrade gracefully if mesages/email isn’t set up” made sense. But Django is moving away from that pattern into using a dictionary of named backends configs (DATABASES, CACHES, STORAGES, TASKS, and soon email with EMAIL_PROVIDERS). In this format, each backend is intentionally configured; its absence is a hard error. The ambiguity that fail_silently was presumably designed to handle over no longer exists.

Given this, I’m inclined to vote for option 2 (deprecate entirely) as my first choice, but if that one is considered too aggressive, I’d go with option 3 as a compromise.

IMHO the classical use case for keeping fail_silently is mail_admins() in an error handler, but I think that is better served by an explicit try/except at the call site, where the caller actually knows the context and intent. At this point, fail_silently=True is just a bundled try/except with worse readability and hidden behavior.

2 Likes

:light_bulb:Ah-hah! That’s what I had been missing, and totally makes sense to me. It would explain why Django uses fail_silently=True in AdminLogHandler and BrokenLinksMiddleware, as well as several examples I saw in the wild.

And it happens to work that way with Django’s default email settings, but…

It’s an expensive way to detect “not configured”: With only default settings, sending mail tries to connect to the SMTP server on localhost without SMTP auth, then tries to transmit a message (typically) from webmaster@localhost or root@localhost. Depending on whether there’s a local SMTP server and how it’s set up, that will raise SMTPConnectError, SMTPAuthenticationError, SMTPSenderRefused, or perhaps TimeoutError after a long pause.

All of these are indeed ignored by fail_silently=True as implemented today. But there are much easier ways to skip sending when email isn’t configured.

fail_silently hides errors that are real problems in configuration, as well as transient issues that prevent mail from getting sent. These are all silent:

  • misconfigured EMAIL_HOST = "smtp.gmail.comm"
  • wrong EMAIL_HOST_PASSWORD and similar auth problems
  • some forms of invalid/unusable DEFAULT_FROM_EMAIL or SERVER_EMAIL
  • broken cert store when using SSL/TLS SMTP
  • dropped network connection while sending

If the caller’s intent is to send email so long as it’s configured, swallowing these errors seems like a mistake. And it’s kind of a bug that AdminLogHandler and BrokenLinksMiddleware ignore them.

fail_silently doesn’t help with recipient errors: I saw a lot of code that seemed to be saying, “send this, but I don’t care if the to address bounces.” But problems with recipient mailboxes aren’t detected at send time[1] (email is store-and-forward, and delivery problems are reported out of band).

If the caller’s intent is ignoring typo emails and “mailbox full” errors, those are already “silent.” Using fail_silently=True is unnecessary and can mask other problems.

fail_silently doesn’t suppress all exceptions: If the caller’s intent is preventing error cascades (e.g., when calling mail_admins() from an error handler), what we’re doing now is insufficient.

To unblock EMAIL_PROVIDERS, we need a plan where the per-send fail_silently modifier is not implemented as backend configuration (not passed to EmailBackend.__init()__). Any of the options in this thread would solve that.

I was hoping to make fail_silently=True behave according to the caller’s intent. For send_mail() and EmailMessage.send(), I don’t think it’s possible to know that intent: everything I listed above is common. So getting rid of fail_silently and forcing callers to decide what they want felt safest. (Gemini invoked “explicit is better than implicit” in its support of this option.)

For mail_admins() and mail_managers(), every use of fail_silently=True I found was inside other error reporting. I thought the intent was to avoid cascades, so catching all exceptions—including configuration errors—seemed reasonable. But I’m rethinking that in light of your observation about the contrib.messages parallel and @nessita’s accurate observation that “fail_silently=True is just a bundled try/except with worse readability and hidden behavior.”

I think I have a new proposal, which I’ll put in a separate message.


  1. Unless you’re dealing with a local SMTP server delivering to a local mailbox, which might raise SMTPRecipientsRefused or SMTPResponseException when sending. (I’d think local delivery is rare.)

    With third party backends, a few ESPs’ APIs enforce recipient block lists and other policies at send time. But most handle that with out of band notification, just like SMTP bounces. ↩︎

2 Likes

Thanks everyone, the discussion has really helped.

New proposal:

  • Django shouldn’t have a default email provider
  • We need a way to send a message when email is configured but avoid an error if it’s not
  • fail_silently is unsalvageable. Deprecate and remove it!

No default email configuration

Django’s current defaults—sending email through an SMTP server on localhost without auth— are not useful in modern infrastructure don’t work with many common server configurations. Your web server[1] probably often isn’t running SMTP. Or if it somehow is, outbound mail almost certainly often requires auth. [Edited in response to Ken’s insights below.]

In a fresh Django project, the current defaults mean trying to send email leads to a cryptic error like “ConnectionRefusedError: [Errno 61] Connection refused.” This comes up often enough that it’s covered in the django-allauth FAQ.

Unlike caches, databases, storages and tasks, there isn’t really a usable email provider to be the default. (We could try something like the console or dummy backends, but I suspect that would be at least as confusing as the current errors.)

I’m going to update the dep to make the default EMAIL_PROVIDERS empty: there is no email provider defined by default. That means trying to send email in a fresh project will raise “InvalidEmailProvider: The email provider ‘default’ doesn’t exist.” (Hopefully no FAQ entry needed​:crossed_fingers:)

(I’m tempted to also add an empty EMAIL_PROVIDERS dict to the settings template, with a comment that enabling email requires a “default” provider.)


“Send email if configured” sending option

Reusable apps need a clean way to say, “I’d like to send some mail, so long as my users have email configured at all.” Django needs it too:[2]

class AdminEmailHandler(logging.Handler):
    def send_mail(self, subject, message, *args, **kwargs):
        if mail.providers.is_configured():
            mail.mail_admins(subject, message, *args, **kwargs)

The new providers.is_configured(using="default") checks if the using alias is defined in EMAIL_PROVIDERS. This approach doesn’t suppress any configuration or sending errors. If you’ve defined a default email provider but have a typo in the host, you want to know about that. Same thing if mail intended for admins is getting dropped because your mail server is unreachable.[3]

[Edit: The original version of this post had a different proposal—meant to accomplish the same thing—here. Check the diffs if you’re interested. Apologies for any confusion.]


Deprecate fail_silently in sending APIs

The semantics of fail_silently=True are unclear and backend dependent. Callers’ intent in using it varies widely—and rarely aligns with its actual behavior. It’s “a language feature (try/except) as an argument” with “worse readability and hidden behavior.”

I’ve seen the light:[4] it’s time to deprecate the fail_silently arg to all sending methods—send_mail(), send_mass_mail(), EmailMessage.send(), mail_admins() and mail_managers().

One nuance: I might keep the current fail_silently implementation inside the smtp EmailBackend, but as configuration only. Someone who needs the “ignore smtp and some OS error” semantics could define an EMAIL_PROVIDERS alias with "fail_silently": True in the OPTIONS. (So long as we deprecate and remove the send-time fail_silently arg, it doesn’t really matter if it’s still a backend option. The dep problem is the mismatch between the two.)


Thoughts?

The first two items would be part of the EMAIL_PROVIDERS dep. We could still deprecate fail_silently ahead of that work.


  1. or development machine ↩︎

  2. I now believe Django’s AdminEmailHandler and BrokenLinkEmailsMiddleware are using fail_silently=True to suppress the cryptic errors caused by Django’s default unconfigured email settings (particularly with AdminEmailHandler, which is enabled by default in production). And not because they want to hide configuration or network errors. But I could be reading it wrong. ↩︎

  3. As a reminder, errors raised from the AdminEmailHandler do not end up back in the AdminEmailHandler—there’s no worry about infinite loops. But they will show up in your server error logs somewhere. ↩︎

  4. The AIs had convinced me removing it entirely would be too disruptive. The humans have convinced me it’s the only pragmatic solution. (Thanks, humans!) ↩︎

3 Likes

My 2 cents on default backends, I would be +1 for a console backend for development purposes and a dummy as example to third-party providers, similar to the current tasks backends.

I would then have a comment to mention about configuring production providers.

1 Like

First and foremost, I’d be personally fine with having a console handler as the built-in default.

But, for background purposes, I would like to address one specific point you made:

I disagree with this statement. Every server I run that has a need to send email has an associated instance of Postfix running, listening on localhost only, with the option of having IPTables controlling access to the port by UID of the sending process.

I do this for a variety of reasons:

  • Running postfix on my server allows me to send emails for my own use without needing to send them through a provider. (Mostly, I’m referring to error-related emails such as the HTTP 500 error emails.) Additionally, since those emails remain local, there’s less risk involved with sensitive data that may be in them.

    • I also run Dovecot to allow for access to emails defined as remaining local. This gives me the ability to run a “staging” environment, where I can test processes that would send emails, but without those emails going anywhere.
  • Using a local connection improves the performance of the Django email functionality. It takes significantly less time to send an email from Django than if Django were connecting to an external email provider. Removing the authentication process from the connection further streamlines the operation.

  • If there’s any problem with the provider’s service or connecting to that service, Postfix will queue the messages and continue to try to send them. Meanwhile, from Django’s perspective, the emails have been successfully sent.

  • This also reduces the number of connections that need to be made to my email provider. Every Django process or thread is only sending locally. The local postfix connection only creates one connection to the email provider.

  • Adding Postfix to my email processing gives me yet one more option for tracking and monitoring email usage. The Postfix logs help me track how many emails are being sent and to whom. I also have the ability to prevent emails from going out after they have been generated.

1 Like

I’m coming around to the view that fail_silently is unsalvagable.

We could still deprecate fail_silently ahead of that work.

Possibly, but if the DEP doesn’t follow hard upon, then we’re deprecating something without providing an upgrade path, which is out of the ordinary for us.

I’ve gone back and forth on this a few times. I’m still leaning toward no default email provider, so that sends fail with a clear error if the user doesn’t explicitly define how email should be handled.

Email is just a little different from tasks, caches, databases, storages and templates:

  • All of their built-in defaults (except TEMPLATES) are at least minimally functional. The defaults may not be production grade, but they’re not fundamentally wrong.
  • Dumping email to console and calling it “sent” is useful in development, but feels broken as a production default. (Also, there might be PII concerns.[1])
  • Since Django swaps out the settings.py email config with the locmem test backend during testing, problems with email configuration often don’t show up until production. (This is one of the few places where developers have to take extra steps to make test behavior match dev/production.)

We could still include console in the settings template for new projects. I just wouldn’t make it an implicit default. (I think we handle the TEMPLATES setting similarly.)

# settings-py.tpl

# Email
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-providers
EMAIL_PROVIDERS = {
    "default": {
        # Prints emails to console. Change this to enable sending real email.
        "BACKEND": "django.core.mail.backends.console.EmailBackend",
    },
}

Maybe also add a deployment check to warn if the default email provider is unset or set to console, dummy or locmem.

Thanks, Ken, it’s really helpful to get perspective from someone running Django and local SMTP on the same box.[2]

So are you running entirely with Django’s default EMAIL_* settings? If so, I think the impact of this proposed change would be:

  • Django 6.1–6.2: startup time deprecation warning: “Django 7.0 will not have a default email provider. Define EMAIL_PROVIDERS in settings.py to continue using the SMTP EmailBackend.”
  • Django 7.0: send-time errors: “InvalidEmailProvider: The email provider ‘default’ doesn’t exist.”

If you’ve defined any of the EMAIL_* in settings.py, the DEP currently says you’ll get deprecation warnings for those on startup.

Is that reasonable? (Unfortunately, I think django-upgrade won’t be able to autofix email settings.)

Understood. I’ll handle it all in the DEP rather than a separate ticket for fail_silently.


  1. I’m not claiming they’re valid concerns. I don’t think Django is responsible for—or capable of—sanitizing personally identifiable information from logs. But from experience with django-anymail, someone would complain. (Also, someone would probably report a security issue that password reset links get logged to console.) ↩︎

  2. I’ve always used django-mailer or django-celery-email to address those concerns, though I’m looking at moving to Django tasks now. (I don’t have the expertise or patience to run Postfix or Sendmail.) ↩︎

1 Like

Nope - at a minimum I set DEFAULT_FROM_EMAIL, SERVER_EMAIL, and ADMINS. Oh, wait - you’re specifying EMAIL_* - yes, my non-docker deployments don’t include any EMAIL_* settings.

I’m ok with any changes made to this.

It’s not a big deal for me to upgrade my settings. Given that I’m just setting settings that already exist as defaults, I can take care of that anytime - including starting now.

For clarity, my comments were not to express any negative opinion of the proposal. I only wanted to document that there are valid reasons for running an smtp server on the same host as your Django processes, and that it’s not an outdated or outmoded idea.

2 Likes

Given Ken’s answer (which was very educational), couldn’t we now say that the existing defaults (mapped to the new EMAIL_PROVIDERS structure as shown below) are “at least minimally functional. The defaults may not be production grade, but they’re not fundamentally wrong”? If we agree on that, then we could define the default to be:

EMAIL_PROVIDERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "localhost",
            "port": 25,
            "username": "",
            "password": "",
            # etc.
        },
    },
}

@jacobtylerwalls you said “[…] without providing an upgrade path”, question: isn’t the upgrade path to do error handling on the call site based on the specific business logic of each project when the boolean is set to True? (which as per Mike’s research is a minority of the cases). I’m inclined to deprecate fail_silently ASAP assuming I’m not missing something here.

Ah, no, I think the upgrade path is this new goodie from Mike’s DEP:

Since there is no project-specific business logic that is knowable for a reusable app:

Sorry but I don’t follow. My point (which perhaps I did not express properly before, my bad!) is that even for a reusable app, the project-specific business logic is “if email can be sent, send an email” which to me that roughly is:

try:
    send_email(...)
except Exception as e:  # or OSError, or SMTError, or ...
    logger.info("No email sent for X due to %s", e)

Or, if sending the email is crucial for the reusable app business logic, the reusable app may log WARNING or even EXCEPTION or ultimately let it raise if it makes sense (as is or via a project-specific exception).

In my view, the above is the upgrade path independently of the DEP. How is that different than the current fail_silently?

I actually don’t agree on that: I now believe that defaulting to the SMTP backend on localhost is fundamentally wrong.

With those defaults, the only way to detect whether email is available is to try to send a message and look for certain exceptions. That’s an expensive and possibly slow test. It’s also problematic because it treats “I haven’t configured email” and “I have configured email but got some settings wrong” as equivalent.

Imho, what I now believe reusable apps and Django’s AdminLogHandler are trying to say is, “if email capabilities are configured, send an email.” I wouldn’t think they’d want to mask configuration and operational errors in those sends—that behavior always confused me. But when defaulting to SMTP on localhost, “email capabilities are not configured” is indistinguishable from several other error conditions, all of which got swept up in fail_silently.

There was a time when server OS distributions often (usually?) included an SMTP server preinstalled and configured for delivery to local mailboxes. In that context, Django’s default email config was minimally functional (and not fundamentally wrong). Today, many (most?) OS distributions used for running web servers don’t come with local SMTP by default. Whether to install SMTP and how to configure it is very much an operational decision. (This was the point I was trying to make, but worded poorly, in my earlier comment.)

For projects that run without a local SMTP server (which is not uncommon), Django’s current email defaults result in non-obvious errors like “Connection refused.” And they make it impossible to distinguish “email unconfigured” vs “email configured but broken.” That’s why I think we need to change the defaults. And EMAIL_PROVIDERS gives us an opportunity to do it with appropriate deprecations.

1 Like

For me, that’s not an upgrade path, that’s a removal path. Django will be shipping a superior solution than the try/except: is_configured(). It’s needless churn to ask users to go through a removal path for a feature when we plan to ship an replacement for a feature in short order. That’s why I said it was “out of the ordinary” to advance these pieces in separate releases. I’m not digging in my heels – maybe there’s a compelling reason to do it – I’m just saying I question the reason. Nothing should prevent us from reviewing and approving the work in separate chunks like we did for other complex features.

Super minor point to be debating, so I’m happy to leave it for some other time :smiley:

1 Like

Thank you, this convinced me. I appreciate the patience and the thoughtful discussion. :trophy:

Right, thank you for the reminder. We seem aligned on the overall picture, so I agree there is no need to block or iterate on this at this time. :heart_hands:

2 Likes