Skip to content

Commit

Permalink
added auto html to plain text mail generation
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Aug 26, 2022
1 parent f14105b commit 0f9ddbf
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 14 deletions.
105 changes: 105 additions & 0 deletions tools/mailer/html2text.go
Original file line number Diff line number Diff line change
@@ -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
}
131 changes: 131 additions & 0 deletions tools/mailer/html2text_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
5 changes: 5 additions & 0 deletions tools/mailer/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion ui/.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ PB_PROFILE_COLLECTION = "profiles"
PB_INSTALLER_PARAM = "installer"
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/manage-collections#rules-filters-syntax"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.5.1"
PB_VERSION = "v0.6.0"

Large diffs are not rendered by default.

Loading

0 comments on commit 0f9ddbf

Please sign in to comment.