diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 549b8050fe95..2b396d870b43 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -360,8 +360,14 @@ def message(self, *, policy=email.policy.default): # Use cached DNS_NAME for performance msg["Message-ID"] = make_msgid(domain=DNS_NAME) for name, value in self.extra_headers.items(): + header = name.lower() + if header == "bcc": + raise ValueError( + 'Bcc is not a valid email header. Use the "bcc" ' + "argument to specify blind carbon copy recipients." + ) # Avoid headers handled above. - if name.lower() not in {"from", "to", "cc", "reply-to"}: + if header not in {"from", "to", "cc", "reply-to"}: msg[name] = force_str(value, strings_only=True) self._idna_encode_address_header_domains(msg) return msg diff --git a/django/views/decorators/csp.py b/django/views/decorators/csp.py index 1c537fe1f2c5..c6ee54fea4b6 100644 --- a/django/views/decorators/csp.py +++ b/django/views/decorators/csp.py @@ -9,11 +9,15 @@ def _make_csp_decorator(config_attr_name, config_attr_value): raise TypeError("CSP config should be a mapping.") def decorator(view_func): - @wraps(view_func) - async def _wrapped_async_view(request, *args, **kwargs): - response = await view_func(request, *args, **kwargs) - setattr(response, config_attr_name, config_attr_value) - return response + if iscoroutinefunction(view_func): + + @wraps(view_func) + async def _wrapped_async_view(request, *args, **kwargs): + response = await view_func(request, *args, **kwargs) + setattr(response, config_attr_name, config_attr_value) + return response + + return _wrapped_async_view @wraps(view_func) def _wrapped_sync_view(request, *args, **kwargs): @@ -21,8 +25,6 @@ def _wrapped_sync_view(request, *args, **kwargs): setattr(response, config_attr_name, config_attr_value) return response - if iscoroutinefunction(view_func): - return _wrapped_async_view return _wrapped_sync_view return decorator diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 846ac0973b3f..ef448c6f7904 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1244,8 +1244,11 @@ subclass:: This should be set to a list of field names that will be searched whenever somebody submits a search query in that text box. - These fields should be some kind of text field, such as ``CharField`` or - ``TextField``. You can also perform a related lookup on a ``ForeignKey`` or + These fields are typically text fields such as ``CharField`` or + ``TextField``, but non-text fields like ``IntegerField`` can also be + searched. Adding an explicit lookup, such as ``age__exact``, lets the + search use that lookup directly instead of casting the value to text. You + can also perform a related lookup on a ``ForeignKey`` or ``ManyToManyField`` with the lookup API "follow" notation:: search_fields = ["foreign_key__related_fieldname"] @@ -1257,17 +1260,23 @@ subclass:: search_fields = ["user__email"] When somebody does a search in the admin search box, Django splits the - search query into words and returns all objects that contain each of the - words, case-insensitive (using the :lookup:`icontains` lookup), where each - word must be in at least one of ``search_fields``. For example, if - ``search_fields`` is set to ``['first_name', 'last_name']`` and a user - searches for ``john lennon``, Django will do the equivalent of this SQL - ``WHERE`` clause: + search query into search terms and returns all objects that contain each + of them in at least one of ``search_fields``. By default, the + :lookup:`icontains` lookup is used for text fields. For non-text fields + with an explicit lookup (e.g., ``age__exact``), the specified lookup is + used, and search terms that are invalid for that field type are skipped. + + For example, if ``search_fields`` is set to + ``['first_name', 'last_name', 'age__exact']`` and a user searches for + ``john 25``, Django will do the equivalent of this SQL ``WHERE`` clause: .. code-block:: sql WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%') - AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%') + AND (first_name ILIKE '%25%' OR last_name ILIKE '%25%' OR age = 25) + + Note that ``john`` is not searched on ``age`` because it's not a valid + integer, while ``25`` is searched on all three fields. The search query can contain quoted phrases with spaces. For example, if a user searches for ``"john winston"`` or ``'john winston'``, Django will do @@ -1298,6 +1307,11 @@ subclass:: :meth:`ModelAdmin.get_search_results` to provide additional or alternate search behavior. + .. versionchanged:: 6.1 + + On earlier versions, search terms invalid for a given field type were + not skipped. + .. attribute:: ModelAdmin.search_help_text Set ``search_help_text`` to specify a descriptive text for the search box diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index adb66a013d31..03a9b8cdfd0a 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -470,6 +470,10 @@ Email ``EmailMessage``. (This behavior was never documented. The ``send()`` method will still *use* a ``connection`` that is set on the message before sending.) +* :meth:`.EmailMessage.message` now raises a ``ValueError`` if ``Bcc`` is + included in the ``headers`` argument or ``extra_headers`` attribute. Use the + ``bcc`` argument instead. + Models ------ diff --git a/docs/topics/email.txt b/docs/topics/email.txt index d52ffd12ed9d..c9e3cfd1cef0 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -505,7 +505,7 @@ email backend API :ref:`provides an alternative * ``cc``: A list or tuple of recipient addresses used in the "Cc" header when sending the email. - * ``bcc``: A list or tuple of addresses used in the "Bcc" header when + * ``bcc``: A list or tuple of addresses used for blind carbon copies when sending the email. * ``reply_to``: A list or tuple of recipient addresses used in the diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 7963b81892c1..01a37284c10f 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -397,6 +397,18 @@ def test_bcc_not_in_headers(self): self.assertNotIn("bcc@example.com", message.as_string()) self.assertEqual(email.recipients(), ["to@example.com", "bcc@example.com"]) + def test_bcc_in_headers_raises_error(self): + email = EmailMessage( + to=["to@example.com"], + headers={"Bcc": "bcc@example.com"}, + ) + msg = ( + 'Bcc is not a valid email header. Use the "bcc" argument to ' + "specify blind carbon copy recipients." + ) + with self.assertRaisesMessage(ValueError, msg): + email.message() + def test_reply_to(self): with self.subTest("Single Reply-To"): email = EmailMessage(