WIP
This commit is contained in:
parent
a75fb08ef1
commit
d88dbbc90f
4 changed files with 200 additions and 11 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -492,6 +492,131 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
|
|||
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_HTMLEmail(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: test@mydomain.me
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Message-Id: <51610934ss4.mmailer@fritz.box>
|
||||
From: <email@email.com>
|
||||
To: <email@email.com>,
|
||||
<ntfy-subjectatntfy@ntfy.sh>
|
||||
Date: Thu, 30 Mar 2023 02:56:53 +0000
|
||||
Subject: A HTML email
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<=21DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Alerttitle</title>
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html;charset=3Dutf-8"/>
|
||||
</head>
|
||||
<body style=3D"color: =23000000; background-color: =23f0eee6;">
|
||||
<table width=3D"100%" align=3D"center" style=3D"border:solid 2px =23eeeeee=
|
||||
; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td>
|
||||
<table style=3D"border-collapse: collapse;">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"background: =23FFFFFF;">
|
||||
<table style=3D"color: =23FFFFFF; background-color: =23006EC0; border-coll=
|
||||
apse: collapse;">
|
||||
<tr>
|
||||
<td style=3D"width: 1000px; text-align: center; font-size: 18pt; font-fami=
|
||||
ly: Arial, Helvetica, sans-serif; padding: 10px;">
|
||||
|
||||
|
||||
headertext of table
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"padding: 10px 20px; background: =23FFFFFF;">
|
||||
<table style=3D"border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style=3D"width: 940px; font-size: 13pt; font-family: Arial, Helvetica,=
|
||||
sans-serif; text-align: left;">
|
||||
" Very important information about a change in your
|
||||
home automation setup
|
||||
|
||||
Now the light is on
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"padding: 10px 20px; background: =23FFFFFF;">
|
||||
<table>
|
||||
<tr>
|
||||
<td style=3D"width: 960px; font-size: 10pt; font-family: Arial, Helvetica,=
|
||||
sans-serif; text-align: left;">
|
||||
<hr />
|
||||
If you don't want to recieve this message anymore, stop the push
|
||||
services in your <a href=3D"https:fritzbox" target=3D"_=
|
||||
blank">FRITZ=21Box</a>=2E<br />
|
||||
Here you can see the active push services: "System > Push Service"=2E
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table style=3D"color: =23FFFFFF; background-color: =23006EC0;">
|
||||
<tr>
|
||||
<td style=3D"width: 1000px; font-size: 10pt; font-family: Arial, Helvetica=
|
||||
, sans-serif; text-align: center; padding: 10px;">
|
||||
This mail has ben sent by your <a style=3D"color: =23FFFFFF;" href=3D"https:=
|
||||
//fritzbox" target=3D"_blank">FRITZ=21Box</a=
|
||||
> automatically=2E
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
.
|
||||
`
|
||||
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "A HTML email", r.Header.Get("Title"))
|
||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_PlaintextWithToken(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue