diff --git a/internal/linkcheck/linkcheck_test.go b/internal/linkcheck/linkcheck_test.go index bf27a8f..563c98b 100644 --- a/internal/linkcheck/linkcheck_test.go +++ b/internal/linkcheck/linkcheck_test.go @@ -31,7 +31,14 @@ var ( ` expectedHTMLLinks = []string{ - "http://example.com", "https://example.com", "HTTPS://EXAMPLE.COM", "http://localhost", "https://localhost", "https://127.0.0.1", "http://link with spaces", "http://example.com/?blaah=yes&test=true", + "http://example.com", + "https://example.com", + "HTTPS://EXAMPLE.COM", + "http://localhost", + "https://localhost", + "https://127.0.0.1", + "http://link with spaces", + "http://example.com/?blaah=yes&test=true", "http://remote-host/style.css", // css "https://example.com/image.jpg", // images } @@ -41,10 +48,18 @@ var ( [http://localhost] www.google.com < ignored |||http://example.com/?some=query-string||| + // RFC2396 appendix E states angle brackets are recommended for text/plain emails to + // recognize potential spaces in between the URL + ` expectedTextLinks = []string{ - "http://example.com", "https://example.com", "HTTPS://EXAMPLE.COM", "http://localhost", "http://example.com/?some=query-string", + "http://example.com", + "https://example.com", + "HTTPS://EXAMPLE.COM", + "http://localhost", + "http://example.com/?some=query-string", + "https://example.com/ link with spaces", } ) diff --git a/internal/linkcheck/main.go b/internal/linkcheck/main.go index eaba8ba..f1c0db3 100644 --- a/internal/linkcheck/main.go +++ b/internal/linkcheck/main.go @@ -30,9 +30,28 @@ func RunTests(msg *storage.Message, followRedirects bool) (Response, error) { } func extractTextLinks(msg *storage.Message) []string { + testLinkRe := regexp.MustCompile(`(?im)([^<]\b)((http|https):\/\/([\-\w@:%_\+'!.~#?,&\/\/=;]+))`) + // RFC2396 appendix E states angle brackets are recommended for text/plain emails to + // recognize potential spaces in between the URL + // @see https://www.rfc-editor.org/rfc/rfc2396#appendix-E + bracketLinkRe := regexp.MustCompile(`(?im)<((http|https):\/\/([\-\w@:%_\+'!.~#?,&\/\/=;][^>]+))>`) + links := []string{} - links = append(links, linkRe.FindAllString(msg.Text, -1)...) + matches := testLinkRe.FindAllStringSubmatch(msg.Text, -1) + for _, match := range matches { + if len(match) > 0 { + links = append(links, match[2]) + } + } + + angleMatches := bracketLinkRe.FindAllStringSubmatch(msg.Text, -1) + for _, match := range angleMatches { + if len(match) > 0 { + link := strings.ReplaceAll(match[1], "\n", "") + links = append(links, link) + } + } return links } diff --git a/server/ui-src/components/message/MessageItem.vue b/server/ui-src/components/message/MessageItem.vue index 643b19e..1369f26 100644 --- a/server/ui-src/components/message/MessageItem.vue +++ b/server/ui-src/components/message/MessageItem.vue @@ -290,15 +290,24 @@ export default { textToHTML(s) { let html = s; - // full links with http(s) - const re = /(\b(https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=;]+)/gim; - html = html.replace(re, "˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲"); + // RFC2396 appendix E states angle brackets are recommended for text/plain emails to + // recognize potential spaces in between the URL + // @see https://www.rfc-editor.org/rfc/rfc2396#appendix-E + const angleLinks = /<((https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=; ][^>]+)>/gim; + html = html.replace(angleLinks, "<˱˱˱a href=ˠˠˠ$1ˠˠˠ target=_blank rel=noopener˲˲˲$1˱˱˱/a˲˲˲>"); + + // find links without angle brackets, starting with http(s) or ftp + const regularLinks = /([^ˠ˲]\b)(((https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=;]+))/gim; + html = html.replace(regularLinks, "$1˱˱˱a href=ˠˠˠ$2ˠˠˠ target=_blank rel=noopener˲˲˲$2˱˱˱/a˲˲˲"); // plain www links without https?:// prefix - const re2 = /(^|[^/])(www\.[\S]+(\b|$))/gim; - html = html.replace(re2, "$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲"); + const shortLinks = /(^|[^/])(www\.[\S]+(\b|$))/gim; + html = html.replace( + shortLinks, + "$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲", + ); - // escape to HTML & convert <>" back + // escape to HTML & convert <>" characters back html = html .replace(/&/g, "&") .replace(/