This commit is contained in:
binwiederhier 2023-04-02 13:59:26 -04:00
parent a75fb08ef1
commit d88dbbc90f
4 changed files with 200 additions and 11 deletions

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/emersion/go-smtp"
"github.com/microcosm-cc/bluemonday"
"io"
"mime"
"mime/multipart"
@ -13,6 +14,7 @@ import (
"net/http"
"net/http/httptest"
"net/mail"
"regexp"
"strings"
"sync"
)
@ -231,37 +233,66 @@ func readMailBody(body io.Reader, header mail.Header) (string, error) {
if err != nil {
return "", err
}
if strings.ToLower(contentType) == "text/plain" {
return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding"))
} else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
return readMultipartMailBody(body, params, 0)
canonicalContentType := strings.ToLower(contentType)
if canonicalContentType == "text/plain" || canonicalContentType == "text/html" {
return readTextMailBody(body, canonicalContentType, header.Get("Content-Transfer-Encoding"))
} else if strings.HasPrefix(canonicalContentType, "multipart/") {
return readMultipartMailBody(body, params)
}
return "", errUnsupportedContentType
}
func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) {
func readMultipartMailBody(body io.Reader, params map[string]string) (string, error) {
parts := make(map[string]string)
if err := readMultipartMailBodyParts(body, params, 0, parts); err != nil && err != io.EOF {
return "", err
} else if s, ok := parts["text/plain"]; ok {
return s, nil
} else if s, ok := parts["text/html"]; ok {
return s, nil
}
return "", io.EOF
}
func readMultipartMailBodyParts(body io.Reader, params map[string]string, depth int, parts map[string]string) error {
if depth >= maxMultipartDepth {
return "", errMultipartNestedTooDeep
return errMultipartNestedTooDeep
}
mr := multipart.NewReader(body, params["boundary"])
for {
part, err := mr.NextPart()
if err != nil { // may be io.EOF
return "", err
return err
}
partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
if err != nil {
return "", err
return err
}
if strings.ToLower(partContentType) == "text/plain" {
return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
canonicalPartContentType := strings.ToLower(partContentType)
if canonicalPartContentType == "text/plain" || canonicalPartContentType == "text/html" {
s, err := readTextMailBody(part, canonicalPartContentType, part.Header.Get("Content-Transfer-Encoding"))
if err != nil {
return err
}
parts[canonicalPartContentType] = s
} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") {
return readMultipartMailBody(part, partParams, depth+1)
if err := readMultipartMailBodyParts(part, partParams, depth+1, parts); err != nil {
return err
}
}
// Continue with next part
}
}
func readTextMailBody(reader io.Reader, contentType, transferEncoding string) (string, error) {
if contentType == "text/plain" {
return readPlainTextMailBody(reader, transferEncoding)
} else if contentType == "text/html" {
return readHTMLMailBody(reader, transferEncoding)
}
return "", fmt.Errorf("unsupported content type: %s", contentType)
}
func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) {
if strings.ToLower(transferEncoding) == "base64" {
reader = base64.NewDecoder(base64.StdEncoding, reader)
@ -272,3 +303,27 @@ func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, e
}
return string(body), nil
}
func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error) {
body, err := readPlainTextMailBody(reader, transferEncoding)
if err != nil {
return "", err
}
stripped := bluemonday.
StrictPolicy().
AddSpaceWhenStrippingTag(true).
Sanitize(body)
return removeExtraEmptyLines(stripped), nil
}
func removeExtraEmptyLines(str string) string {
// Replace lines that contain only spaces with empty lines
re := regexp.MustCompile(`(?m)^\s+$`)
str = re.ReplaceAllString(str, "")
// Remove more than 2 consecutive empty lines
re = regexp.MustCompile(`\n{3,}`)
str = re.ReplaceAllString(str, "\n\n")
return str
}