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_PASSWORDand similar auth problems - some forms of invalid/unusable
DEFAULT_FROM_EMAILorSERVER_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.
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. ↩︎