Parse nested multipart emails, fixes #610
parent
e9b05e8ed7
commit
5f75e98861
|
@ -6,7 +6,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Support for base64 encoded emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts))
|
||||
* Support for base64-encoded and nested multipart emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts))
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
|
|
|
@ -22,9 +22,14 @@ var (
|
|||
errInvalidAddress = errors.New("invalid address")
|
||||
errInvalidTopic = errors.New("invalid topic")
|
||||
errTooManyRecipients = errors.New("too many recipients")
|
||||
errMultipartNestedTooDeep = errors.New("multipart message nested too deep")
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
)
|
||||
|
||||
const (
|
||||
maxMultipartDepth = 2
|
||||
)
|
||||
|
||||
// smtpBackend implements SMTP server methods.
|
||||
type smtpBackend struct {
|
||||
config *Config
|
||||
|
@ -121,7 +126,7 @@ func (s *smtpSession) Data(r io.Reader) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := readMailBody(msg)
|
||||
body, err := readMailBody(msg.Body, msg.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -203,36 +208,42 @@ func (s *smtpSession) withFailCount(fn func() error) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func readMailBody(msg *mail.Message) (string, error) {
|
||||
if msg.Header.Get("Content-Type") == "" {
|
||||
return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding"))
|
||||
func readMailBody(body io.Reader, header mail.Header) (string, error) {
|
||||
if header.Get("Content-Type") == "" {
|
||||
return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding"))
|
||||
}
|
||||
contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
|
||||
contentType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.ToLower(contentType) == "text/plain" {
|
||||
return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding"))
|
||||
return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding"))
|
||||
} else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
|
||||
return readMultipartMailBody(msg, params)
|
||||
return readMultipartMailBody(body, params, 0)
|
||||
}
|
||||
return "", errUnsupportedContentType
|
||||
}
|
||||
|
||||
func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) {
|
||||
mr := multipart.NewReader(msg.Body, params["boundary"])
|
||||
func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) {
|
||||
if depth >= maxMultipartDepth {
|
||||
return "", errMultipartNestedTooDeep
|
||||
}
|
||||
mr := multipart.NewReader(body, params["boundary"])
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err != nil { // may be io.EOF
|
||||
return "", err
|
||||
}
|
||||
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||
partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if strings.ToLower(partContentType) != "text/plain" {
|
||||
continue
|
||||
}
|
||||
return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
|
||||
if strings.ToLower(partContentType) == "text/plain" {
|
||||
return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
|
||||
} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") {
|
||||
return readMultipartMailBody(part, partParams, depth+1)
|
||||
}
|
||||
// Continue with next part
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -390,10 +390,8 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
|
|||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
/*
|
||||
func TestSmtpBackend_NestedMultipartBase64(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
|
||||
func TestSmtpBackend_NestedMultipartBase64(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: test@mydomain.me
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
|
@ -431,14 +429,69 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
|
|||
.
|
||||
`
|
||||
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("This should not be called")
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "451 4.0.0 invalid address")
|
||||
}
|
||||
*/
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local", r.Header.Get("Title"))
|
||||
require.Equal(t, "This is a test message from TrueNAS CORE.", readAll(t, r.Body))
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_NestedMultipartTooDeep(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: test@mydomain.me
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Content-Type: multipart/mixed; boundary="===============1=="
|
||||
MIME-Version: 1.0
|
||||
Subject: TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local
|
||||
From: =?utf-8?q?Robbie?= <test@mydomain.me>
|
||||
To: test@mydomain.me
|
||||
Date: Thu, 16 Feb 2023 01:04:00 -0000
|
||||
Message-ID: <truenas-20230216.010400.344514.b'8jfL'@truenas.local>
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--===============1==
|
||||
Content-Type: multipart/alternative; boundary="===============2=="
|
||||
MIME-Version: 1.0
|
||||
|
||||
--===============2==
|
||||
Content-Type: multipart/alternative; boundary="===============3=="
|
||||
MIME-Version: 1.0
|
||||
|
||||
--===============3==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSBmcm9tIFRydWVOQVMgQ09SRS4=
|
||||
|
||||
--===============3==
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBUcmFuc2l0aW9uYWwv
|
||||
L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
|
||||
|
||||
--===============3==--
|
||||
|
||||
--===============2==--
|
||||
|
||||
--===============1==--
|
||||
.
|
||||
`
|
||||
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("This should not be called")
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep")
|
||||
}
|
||||
|
||||
type smtpHandlerFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) {
|
||||
|
|
Loading…
Reference in New Issue