added auto html to plain text mail generation
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var whitespaceRegex = regexp.MustCompile(`\s+`)
|
||||
|
||||
// Very rudimentary auto HTML to Text mail body converter.
|
||||
//
|
||||
// Caveats:
|
||||
// - This method doesn't check for correctness of the HTML document.
|
||||
// - Links will be converted to "[text](url)" format.
|
||||
// - List items (<li>) are prefixed with "- ".
|
||||
// - Indentation is stripped (both tabs and spaces).
|
||||
// - Trailing spaces are preserved.
|
||||
// - Multiple consequence newlines are collapsed as one unless multiple <br> tags are used.
|
||||
func html2Text(htmlDocument string) (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
doc, err := html.Parse(strings.NewReader(htmlDocument))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tagsToSkip := []string{
|
||||
"style", "script", "iframe", "applet", "object", "svg", "img",
|
||||
"button", "form", "textarea", "input", "select", "option", "template",
|
||||
}
|
||||
|
||||
inlineTags := []string{
|
||||
"a", "span", "small", "strike", "strong",
|
||||
"sub", "sup", "em", "b", "u", "i",
|
||||
}
|
||||
|
||||
var canAddNewLine bool
|
||||
|
||||
// see https://pkg.go.dev/golang.org/x/net/html#Parse
|
||||
var f func(*html.Node)
|
||||
f = func(n *html.Node) {
|
||||
// start link wrapping for producing "[text](link)" formatted string
|
||||
isLink := n.Type == html.ElementNode && n.Data == "a"
|
||||
if isLink {
|
||||
builder.WriteString("[")
|
||||
}
|
||||
|
||||
switch n.Type {
|
||||
case html.TextNode:
|
||||
txt := whitespaceRegex.ReplaceAllString(n.Data, " ")
|
||||
|
||||
// the prev node has new line so it is safe to trim the indentation
|
||||
if !canAddNewLine {
|
||||
txt = strings.TrimLeft(txt, " ")
|
||||
}
|
||||
|
||||
if txt != "" {
|
||||
builder.WriteString(txt)
|
||||
canAddNewLine = true
|
||||
}
|
||||
case html.ElementNode:
|
||||
if n.Data == "br" {
|
||||
// always write new lines when <br> tag is used
|
||||
builder.WriteString("\r\n")
|
||||
canAddNewLine = false
|
||||
} else if canAddNewLine && !list.ExistInSlice(n.Data, inlineTags) {
|
||||
builder.WriteString("\r\n")
|
||||
canAddNewLine = false
|
||||
}
|
||||
|
||||
// prefix list items with dash
|
||||
if n.Data == "li" {
|
||||
builder.WriteString("- ")
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if c.Type != html.ElementNode || !list.ExistInSlice(c.Data, tagsToSkip) {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
|
||||
// end link wrapping
|
||||
if isLink {
|
||||
builder.WriteString("]")
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == "href" {
|
||||
if a.Val != "" {
|
||||
builder.WriteString("(")
|
||||
builder.WriteString(a.Val)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f(doc)
|
||||
|
||||
return strings.TrimSpace(builder.String()), nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHtml2Text(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
html string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"ab c",
|
||||
"ab c",
|
||||
},
|
||||
{
|
||||
"<!-- test html comment -->",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"<!-- test html comment --> a ",
|
||||
"a",
|
||||
},
|
||||
{
|
||||
"<span>a</span>b<span>c</span>",
|
||||
"abc",
|
||||
},
|
||||
{
|
||||
`<a href="a/b/c">test</span>`,
|
||||
"[test](a/b/c)",
|
||||
},
|
||||
{
|
||||
`<a href="">test</span>`,
|
||||
"[test]",
|
||||
},
|
||||
{
|
||||
"<span>a</span> <span>b</span>",
|
||||
"a b",
|
||||
},
|
||||
{
|
||||
"<span>a</span> b <span>c</span>",
|
||||
"a b c",
|
||||
},
|
||||
{
|
||||
"<span>a</span> b <div>c</div>",
|
||||
"a b \r\nc",
|
||||
},
|
||||
{
|
||||
`
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- test html comment -->
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<p>Lorem ipsum</p>
|
||||
<p>Dolor sit amet</p>
|
||||
<p>
|
||||
<a href="a/b/c">Verify</a>
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
<a href="a/b/c"><strong>Verify2.1</strong> <strong>Verify2.2</strong></a>
|
||||
</p>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<ul>
|
||||
<li>ul.test1</li>
|
||||
<li>ul.test2</li>
|
||||
<li>ul.test3</li>
|
||||
</ul>
|
||||
<ol>
|
||||
<li>ol.test1</li>
|
||||
<li>ol.test2</li>
|
||||
<li>ol.test3</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<select>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
</select>
|
||||
<textarea>test</textarea>
|
||||
<input type="text" value="test" />
|
||||
<button>test</button>
|
||||
<p>
|
||||
Thanks,<br/>
|
||||
PocketBase team
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
"Lorem ipsum \r\nDolor sit amet \r\n[Verify](a/b/c) \r\n[Verify2.1 Verify2.2](a/b/c) \r\n\r\n- ul.test1 \r\n- ul.test2 \r\n- ul.test3 \r\n- ol.test1 \r\n- ol.test2 \r\n- ol.test3 \r\nThanks,\r\nPocketBase team",
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result, err := html2Text(s.html)
|
||||
if err != nil {
|
||||
t.Errorf("(%d) Unexpected error %v", i, err)
|
||||
}
|
||||
|
||||
if result != s.expected {
|
||||
t.Errorf("(%d) Expected \n(%q)\n%v,\n\ngot:\n\n(%q)\n%v", i, s.expected, s.expected, result, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,11 @@ func (m *SmtpClient) Send(
|
||||
yak.Subject(subject)
|
||||
yak.HTML().Set(htmlContent)
|
||||
|
||||
// try to generate a plain text version of the HTML
|
||||
if plain, err := html2Text(htmlContent); err == nil {
|
||||
yak.Plain().Set(plain)
|
||||
}
|
||||
|
||||
for name, data := range attachments {
|
||||
yak.Attach(name, data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user