From a2bf5b8118143caa619b41f20e1bef6bc03d85d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 12:00:54 +0100 Subject: [PATCH] [net7.0] Fix e-mail URI escaping recipients (#15244) * Fix e-mail URI escaping recipients Escaping the recipient e-mail addresses, leads to the character @ being escaped and creates an invalid URL * Create proper RFC 2368 mailto url * Add unit tests * Optimize check if any items are present in collection --------- Co-authored-by: Tomasz Cielecki --- src/Essentials/src/Email/Email.shared.cs | 36 ++-- src/Essentials/test/UnitTests/Email_Tests.cs | 187 +++++++++++++++++++ 2 files changed, 204 insertions(+), 19 deletions(-) diff --git a/src/Essentials/src/Email/Email.shared.cs b/src/Essentials/src/Email/Email.shared.cs index a6083c24b5fc..d443f610f742 100644 --- a/src/Essentials/src/Email/Email.shared.cs +++ b/src/Essentials/src/Email/Email.shared.cs @@ -60,31 +60,29 @@ public Task ComposeAsync(EmailMessage? message) return PlatformComposeAsync(message); } - static string GetMailToUri(EmailMessage message) - { - if (message != null && message.BodyFormat != EmailBodyFormat.PlainText) - throw new FeatureNotSupportedException("Only EmailBodyFormat.PlainText is supported if no email account is set up."); + internal static string GetMailToUri(EmailMessage message) => + "mailto:?" + string.Join("&", Parameters(message)); - var parts = new List(); - if (!string.IsNullOrEmpty(message?.Body)) - parts.Add("body=" + Uri.EscapeDataString(message!.Body)); - if (!string.IsNullOrEmpty(message?.Subject)) - parts.Add("subject=" + Uri.EscapeDataString(message!.Subject)); - if (message?.Cc?.Count > 0) - parts.Add("cc=" + Uri.EscapeDataString(string.Join(",", message.Cc))); - if (message?.Bcc?.Count > 0) - parts.Add("bcc=" + Uri.EscapeDataString(string.Join(",", message.Bcc))); + static IEnumerable Parameters(EmailMessage message) + { + if (message.To?.Count > 0) + yield return "to=" + Recipients(message.To); - var uri = "mailto:"; + if (message.Cc?.Count > 0) + yield return "cc=" + Recipients(message.Cc); - if (message?.To?.Count > 0) - uri += Uri.EscapeDataString(string.Join(",", message.To)); + if (message.Bcc?.Count > 0) + yield return "bcc=" + Recipients(message.Bcc); - if (parts.Count > 0) - uri += "?" + string.Join("&", parts); + if (!string.IsNullOrWhiteSpace(message.Subject)) + yield return "subject=" + Uri.EscapeDataString(message.Subject); - return uri; + if (!string.IsNullOrWhiteSpace(message.Body)) + yield return "body=" + Uri.EscapeDataString(message.Body); } + + static string Recipients(IEnumerable addresses) => + string.Join(",", addresses.Select(Uri.EscapeDataString)); } /// diff --git a/src/Essentials/test/UnitTests/Email_Tests.cs b/src/Essentials/test/UnitTests/Email_Tests.cs index f1fe8a4ac6ee..b19a9d86b49e 100644 --- a/src/Essentials/test/UnitTests/Email_Tests.cs +++ b/src/Essentials/test/UnitTests/Email_Tests.cs @@ -1,8 +1,195 @@ +using System; +using System.Collections.Generic; +using Microsoft.Maui.ApplicationModel.Communication; using Xunit; namespace Tests { + public class EmailDataGenerator : IEnumerable + { + private readonly List _data = new() + { + // empty + new object[] + { + new EmailMessage(), + "mailto:?" + }, + + // body only + new object[] + { + new EmailMessage { Body = "Hello" }, + "mailto:?body=Hello" + }, + + // subject only + new object[] + { + new EmailMessage { Subject = "Hello" }, + "mailto:?subject=Hello" + }, + + // subject and body + new object[] + { + new EmailMessage { Subject = "Hello", Body = "Yo" }, + "mailto:?subject=Hello&body=Yo" + }, + + // to only + new object[] + { + new EmailMessage { To = new List { "john@doe.net" } }, + "mailto:?to=john%40doe.net" + }, + + // cc only + new object[] + { + new EmailMessage { Cc = new List { "john@doe.net" } }, + "mailto:?cc=john%40doe.net" + }, + + // bcc only + new object[] + { + new EmailMessage { Bcc = new List { "john@doe.net" } }, + "mailto:?bcc=john%40doe.net" + }, + + // To 1 recipient + new object[] + { + new EmailMessage + { + To = new List { "sauron@mordor.gov.middleearth" }, + Subject = "Claim your free rings of power", + Body = "Click this link to get your rings..." + }, + "mailto:?to=sauron%40mordor.gov.middleearth&subject=Claim%20your%20free%20rings%20of%20power&body=Click%20this%20link%20to%20get%20your%20rings..." + }, + + // To 2 recipients + new object[] + { + new EmailMessage + { + To = new List { "bilbo@hobbiton.shire", "frodo@hobbiton.shire" }, + Subject = "Greetings", + Body = "Greetings Hobbits!" + }, + "mailto:?to=bilbo%40hobbiton.shire,frodo%40hobbiton.shire&subject=Greetings&body=Greetings%20Hobbits%21" + }, + + // Cc 1 recipient + new object[] + { + new EmailMessage + { + Cc = new List { "surfer@maui.net" }, + Subject = "Big waves", + Body = "Dude, there were huge waves yesterday" + }, + "mailto:?cc=surfer%40maui.net&subject=Big%20waves&body=Dude%2C%20there%20were%20huge%20waves%20yesterday" + }, + + // Cc 2 recipients + new object[] + { + new EmailMessage + { + Cc = new List { "surfer@maui.net", "dude@surf.net" }, + Subject = "Duuuude", + Body = "Sweet" + }, + "mailto:?cc=surfer%40maui.net,dude%40surf.net&subject=Duuuude&body=Sweet" + }, + + // Bcc 1 recipient + new object[] + { + new EmailMessage + { + Bcc = new List { "knights@who.say.ni" }, + Subject = "Shrubberies here!", + Body = "Ekke Ekke Ekke Ekke Ptang Zoo Boing!" + }, + "mailto:?bcc=knights%40who.say.ni&subject=Shrubberies%20here%21&body=Ekke%20Ekke%20Ekke%20Ekke%20Ptang%20Zoo%20Boing%21" + }, + + // Bcc 2 recipients + new object[] + { + new EmailMessage + { + Bcc = new List { "knights@who.say.ni", "arthur@who.says.nu" + }, + Subject = "Shrubberies here!", + Body = "Ekke Ekke Ekke Ekke Ptang Zoo Boing!" + }, + "mailto:?bcc=knights%40who.say.ni,arthur%40who.says.nu&subject=Shrubberies%20here%21&body=Ekke%20Ekke%20Ekke%20Ekke%20Ptang%20Zoo%20Boing%21" + }, + + // Mixed recipients + new object[] + { + new EmailMessage + { + To = new List { "bilbo@hobbiton.shire", "frodo@hobbiton.shire" }, + Cc = new List { "knights@who.say.ni", "arthur@who.says.nu" }, + Subject = "Greetings", Body = "Greetings Hobbits!" + }, + "mailto:?to=bilbo%40hobbiton.shire,frodo%40hobbiton.shire&cc=knights%40who.say.ni,arthur%40who.says.nu&subject=Greetings&body=Greetings%20Hobbits%21" + }, + }; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + public class Email_Tests { + [Theory] + [ClassData(typeof(EmailDataGenerator))] + public void GetMailToUri_Returns_RFC2368_Valid_Url(EmailMessage message, string expectedUrl) + { + var result = EmailImplementation.GetMailToUri(message); + + Assert.Equal(expectedUrl, result); + } + + [Fact] + public void GetMailToUri_Ignores_Attachments() + { + var message = new EmailMessage + { + To = new List { "mom@maui.net" }, + Attachments = new List + { + new EmailAttachment("/my/lovely/path/selfie.jpeg") + } + }; + + var result = EmailImplementation.GetMailToUri(message); + + Assert.DoesNotContain("selfie", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public void GetMailToUri_Ingores_BodyFormat() + { + var message = new EmailMessage + { + To = new List { "mom@maui.net" }, + BodyFormat = EmailBodyFormat.Html, + Body = "Hi Mom!" + }; + + var result = EmailImplementation.GetMailToUri(message); + + Assert.Contains("Hi%20Mom%21", result, StringComparison.InvariantCulture); + } } }