Repository: wagtail/wagtail

Issue:
Unable to add parameters to mailto links
### Issue Summary

Using the rich text editor, you cannot add additional URI parameters to a `mailto` link such as subject, bcc, cc, etc. You can only pass a single email address.

### Steps to Reproduce

1.  Create a page or other model with a rich text field, then edit an instance of it
2. Add text, then hyperlink it
3. Try to add an email link with additional parameters e.g. `help@example.com?subject=Help+Me` or `help@example.com?cc=support@example.com`
4. You're told the email is invalid
5. You're also unable to add an analogous URI but with the "mailto:" scheme under External Link

I think the solution could be to validate not against an email address, but a mailto URI. I looked at the Draftail repo first but decided to open the issue here, since it seems that the link selection dialog is native to Wagtail and not the editor itself; forgive me if I'm wrong about that.

Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?

* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: (yes / no)

No, but we are on the latest release (2.8).

### Technical details

* Python version: 3.5.2
* Django version: 2.2.11
* Wagtail version: 2.8
* Browser version: Chrome 80
Unable to add parameters to mailto links
### Issue Summary

Using the rich text editor, you cannot add additional URI parameters to a `mailto` link such as subject, bcc, cc, etc. You can only pass a single email address.

### Steps to Reproduce

1.  Create a page or other model with a rich text field, then edit an instance of it
2. Add text, then hyperlink it
3. Try to add an email link with additional parameters e.g. `help@example.com?subject=Help+Me` or `help@example.com?cc=support@example.com`
4. You're told the email is invalid
5. You're also unable to add an analogous URI but with the "mailto:" scheme under External Link

I think the solution could be to validate not against an email address, but a mailto URI. I looked at the Draftail repo first but decided to open the issue here, since it seems that the link selection dialog is native to Wagtail and not the editor itself; forgive me if I'm wrong about that.

Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?

* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: (yes / no)

No, but we are on the latest release (2.8).

### Technical details

* Python version: 3.5.2
* Django version: 2.2.11
* Wagtail version: 2.8
* Browser version: Chrome 80


Discussion:
Hi @phette23 - thanks for the suggestion. A potential issue with having users enter mailto: links is that editor-level users are not necessarily technical, and may not know how to construct them. I think the way forward would be to have a separate optional field for subject / cc in the link chooser, which would then store the link internally as something like `<a linktype="email" email="foo@example.com" subject="Help me">` - we can then use the existing link-rewriting logic (as used for page and document links) to convert this into a working mailto: URL.
PR raised a while ago with a partial solution - would be good if anyone is keen to pick that up and redo on the latest code + fix one of the minor feedback items.

https://github.com/wagtail/wagtail/pull/6451
Hi @phette23 - thanks for the suggestion. A potential issue with having users enter mailto: links is that editor-level users are not necessarily technical, and may not know how to construct them. I think the way forward would be to have a separate optional field for subject / cc in the link chooser, which would then store the link internally as something like `<a linktype="email" email="foo@example.com" subject="Help me">` - we can then use the existing link-rewriting logic (as used for page and document links) to convert this into a working mailto: URL.
PR raised a while ago with a partial solution - would be good if anyone is keen to pick that up and redo on the latest code + fix one of the minor feedback items.

https://github.com/wagtail/wagtail/pull/6451

Resolving patch:
diff --git a/wagtail/admin/forms/choosers.py b/wagtail/admin/forms/choosers.py
--- a/wagtail/admin/forms/choosers.py
+++ b/wagtail/admin/forms/choosers.py
@@ -44,6 +44,8 @@ class AnchorLinkChooserForm(forms.Form):
 class EmailLinkChooserForm(forms.Form):
     email_address = forms.EmailField(required=True)
     link_text = forms.CharField(required=False)
+    subject = forms.CharField(required=False)
+    body = forms.CharField(required=False, widget=forms.Textarea(attrs={"rows": 3}))
 
 
 class PhoneLinkChooserForm(forms.Form):
diff --git a/wagtail/admin/views/chooser.py b/wagtail/admin/views/chooser.py
--- a/wagtail/admin/views/chooser.py
+++ b/wagtail/admin/views/chooser.py
@@ -1,4 +1,5 @@
 import re
+import urllib.parse as urlparse
 
 from django.conf import settings
 from django.core.paginator import InvalidPage, Paginator
@@ -734,9 +735,66 @@ class EmailLinkView(BaseLinkFormView):
     step_name = "email_link"
     link_url_field_name = "email_address"
 
+    def get_initial_data(self):
+        parsed_email = self.parse_email_link(self.request.GET.get("link_url", ""))
+        return {
+            "email_address": parsed_email["email"],
+            "link_text": self.request.GET.get("link_text", ""),
+            "subject": parsed_email["subject"],
+            "body": parsed_email["body"],
+        }
+
     def get_url_from_field_value(self, value):
         return "mailto:" + value
 
+    def get_result_data(self):
+        params = {
+            "subject": self.form.cleaned_data["subject"],
+            "body": self.form.cleaned_data["body"],
+        }
+        encoded_params = urlparse.urlencode(
+            {k: v for k, v in params.items() if v is not None and v != ""},
+            quote_via=urlparse.quote,
+        )
+
+        url = "mailto:" + self.form.cleaned_data["email_address"]
+        if encoded_params:
+            url += "?" + encoded_params
+
+        return {
+            "url": url,
+            "title": self.form.cleaned_data["link_text"].strip()
+            or self.form.cleaned_data["email_address"],
+            # If the user has explicitly entered / edited something in the link_text field,
+            # always use that text. If not, we should favour keeping the existing link/selection
+            # text, where applicable.
+            "prefer_this_title_as_link_text": ("link_text" in self.form.changed_data),
+        }
+
+    def post(self, request):
+        self.form = self.form_class(
+            request.POST, initial=self.get_initial_data(), prefix=self.form_prefix
+        )
+
+        if self.form.is_valid():
+            result = self.get_result_data()
+            return self.render_chosen_response(result)
+        else:  # form invalid
+            return self.render_form_response()
+
+    def parse_email_link(self, mailto):
+        result = {}
+
+        mail_result = urlparse.urlparse(mailto)
+
+        result["email"] = mail_result.path
+
+        query = urlparse.parse_qs(mail_result.query)
+        result["subject"] = query["subject"][0] if "subject" in query else ""
+        result["body"] = query["body"][0] if "body" in query else ""
+
+        return result
+
 
 class PhoneLinkView(BaseLinkFormView):
     form_prefix = "phone-link-chooser"


Test patch:
diff --git a/wagtail/admin/tests/test_page_chooser.py b/wagtail/admin/tests/test_page_chooser.py
--- a/wagtail/admin/tests/test_page_chooser.py
+++ b/wagtail/admin/tests/test_page_chooser.py
@@ -1,4 +1,5 @@
 import json
+import urllib.parse as urlparse
 
 from django.contrib.auth import get_user_model
 from django.test import TestCase, TransactionTestCase, override_settings
@@ -869,6 +870,79 @@ def test_create_link(self):
         )  # When link text is given, it is used
         self.assertIs(result["prefer_this_title_as_link_text"], True)
 
+    def test_create_link_with_subject_and_body(self):
+        response = self.post(
+            {
+                "email-link-chooser-email_address": "example@example.com",
+                "email-link-chooser-link_text": "contact",
+                "email-link-chooser-subject": "Awesome Subject",
+                "email-link-chooser-body": "An example body",
+            }
+        )
+        self.assertEqual(response.status_code, 200)
+        result = json.loads(response.content.decode())["result"]
+        url = result["url"]
+        self.assertEqual(
+            url,
+            "mailto:example@example.com?subject=Awesome%20Subject&body=An%20example%20body",
+        )
+        self.assertEqual(
+            result["title"], "contact"
+        )  # When link text is given, it is used
+        self.assertIs(result["prefer_this_title_as_link_text"], True)
+
+        mail_parts = urlparse.urlparse(url)
+        query = urlparse.parse_qs(mail_parts.query)
+        self.assertEqual(mail_parts.path, "example@example.com")
+        self.assertEqual(query["subject"][0], "Awesome Subject")
+        self.assertEqual(query["body"][0], "An example body")
+
+    def test_create_link_with_subject_only(self):
+        response = self.post(
+            {
+                "email-link-chooser-email_address": "example@example.com",
+                "email-link-chooser-link_text": "contact",
+                "email-link-chooser-subject": "Awesome Subject",
+            }
+        )
+        self.assertEqual(response.status_code, 200)
+        result = json.loads(response.content.decode())["result"]
+        url = result["url"]
+        self.assertEqual(url, "mailto:example@example.com?subject=Awesome%20Subject")
+        self.assertEqual(
+            result["title"], "contact"
+        )  # When link text is given, it is used
+        self.assertIs(result["prefer_this_title_as_link_text"], True)
+
+        mail_parts = urlparse.urlparse(url)
+        query = urlparse.parse_qs(mail_parts.query)
+        self.assertEqual(mail_parts.path, "example@example.com")
+        self.assertEqual(query["subject"][0], "Awesome Subject")
+        self.assertTrue("body" not in query)
+
+    def test_create_link_with_body_only(self):
+        response = self.post(
+            {
+                "email-link-chooser-email_address": "example@example.com",
+                "email-link-chooser-link_text": "contact",
+                "email-link-chooser-body": "An example body",
+            }
+        )
+        self.assertEqual(response.status_code, 200)
+        result = json.loads(response.content.decode())["result"]
+        url = result["url"]
+        self.assertEqual(url, "mailto:example@example.com?body=An%20example%20body")
+        self.assertEqual(
+            result["title"], "contact"
+        )  # When link text is given, it is used
+        self.assertIs(result["prefer_this_title_as_link_text"], True)
+
+        mail_parts = urlparse.urlparse(url)
+        query = urlparse.parse_qs(mail_parts.query)
+        self.assertEqual(mail_parts.path, "example@example.com")
+        self.assertEqual(query["body"][0], "An example body")
+        self.assertTrue("subject" not in query)
+
     def test_create_link_without_text(self):
         response = self.post(
             {"email-link-chooser-email_address": "example@example.com"}