Repository: django/django
Issue category: Bug Report

Issue:
Urlize incorrectly handles punctuation in email addresses
Description
	 
Several punctuation characters (%, &, +, !, etc.) can—and sometimes do—appear in the local part of an email address (before the @). The urlize template filter doesn't correctly encode them, which can result in broken mailto links.
Example (Django 5.1.4):
from django.template.defaultfilters import urlize
urlize("it%dept@example.org")
# '<a href="mailto:it%dept@example.org">it%dept@example.org</a>'
# Expected:
# '<a href="mailto:it%25dept@example.org">it%dept@example.org</a>'
Clicking the resulting mailto link might work as expected, or do nothing, or could launch a mail composer with a missing or incorrect email address, depending on the specific address, browser and email app. Sequences that could also be percent-encoded characters (like "%de" in the example) are especially prone to unpredictable results.
The mailto URI spec ​RFC 6068 requires percent encoding most punctuation in this situation (section 2, item 1; also see section 5, Encoding).
Proposed fix: apply urllib.parse.quote() to local where the ​mailto link is constructed in django.utils.html.Urlizer. (Although not strictly necessary, it wouldn't hurt to also quote domain there.)


Hints:

Patch:
diff --git a/django/utils/html.py b/django/utils/html.py
index bc336d88a66c..0d107a0da9fe 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -357,6 +357,8 @@ def handle_word(
                     domain = punycode(domain)
                 except UnicodeError:
                     return word
+                local = quote(local, safe="")
+                domain = quote(domain, safe="")
                 url = self.mailto_template.format(local=local, domain=domain)
                 nofollow_attr = ""
             # Make link.


Test patch:
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index dc3768e6fae0..0beaf98bff2b 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -376,6 +376,19 @@ def test_urlize(self):
                 + "한.글." * 15
                 + "aaa</a>",
             ),
+            (
+                # RFC 6068 requires a mailto URI to percent-encode a number of
+                # characters that can appear in <addr-spec>.
+                "yes;this=is&a%valid!email@example.com",
+                '<a href="mailto:yes%3Bthis%3Dis%26a%25valid%21email@example.com"'
+                ">yes;this=is&a%valid!email@example.com</a>",
+            ),
+            (
+                # Urlizer shouldn't urlize the "?org" part of this. But since
+                # it does, RFC 6068 requires percent encoding the "?".
+                "test@example.com?org",
+                '<a href="mailto:test@example.com%3Forg">test@example.com?org</a>',
+            ),
         )
         for value, output in tests:
             with self.subTest(value=value):