diff --git a/docs/releases.md b/docs/releases.md index b294ae16..d9dad6c9 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,6 +4,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## ntfy server v2.0.2 (UNRELEASED) +**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)) + **Additional languages:** * Arabic (thanks to [@ButterflyOfFire](https://hosted.weblate.org/user/ButterflyOfFire/)) diff --git a/server/smtp_server.go b/server/smtp_server.go index f6cfb89f..aefe3147 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "encoding/base64" "errors" "fmt" "github.com/emersion/go-smtp" @@ -204,28 +205,20 @@ func (s *smtpSession) withFailCount(fn func() error) error { func readMailBody(msg *mail.Message) (string, error) { if msg.Header.Get("Content-Type") == "" { - return readPlainTextMailBody(msg) + return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) } contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) if err != nil { return "", err } - if contentType == "text/plain" { - return readPlainTextMailBody(msg) - } else if strings.HasPrefix(contentType, "multipart/") { + if strings.ToLower(contentType) == "text/plain" { + return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) + } else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") { return readMultipartMailBody(msg, params) } return "", errUnsupportedContentType } -func readPlainTextMailBody(msg *mail.Message) (string, error) { - body, err := io.ReadAll(msg.Body) - if err != nil { - return "", err - } - return string(body), nil -} - func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) { mr := multipart.NewReader(msg.Body, params["boundary"]) for { @@ -236,14 +229,20 @@ func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) if err != nil { return "", err - } - if partContentType != "text/plain" { + } else if strings.ToLower(partContentType) != "text/plain" { continue } - body, err := io.ReadAll(part) - if err != nil { - return "", err - } - return string(body), nil + return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding")) } } + +func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) { + if strings.ToLower(transferEncoding) == "base64" { + reader = base64.NewDecoder(base64.StdEncoding, reader) + } + body, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index 80aa6455..8932d726 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -348,6 +348,97 @@ what's up writeAndReadUntilLine(t, email, c, scanner, "451 4.0.0 invalid address") } +func TestSmtpBackend_Base64Body(t *testing.T) { + email := `EHLO example.com +MAIL FROM: test@mydomain.me +RCPT TO: ntfy-mytopic@ntfy.sh +DATA +Content-Type: multipart/mixed; boundary="===============2138658284696597373==" +MIME-Version: 1.0 +Subject: TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local +From: =?utf-8?q?Robbie?= +To: test@mydomain.me +Date: Thu, 16 Feb 2023 01:04:00 -0000 +Message-ID: + +This is a multi-part message in MIME format. +--===============2138658284696597373== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 + +VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSBmcm9tIFRydWVOQVMgQ09SRS4= + +--===============2138658284696597373== +Content-Type: text/html; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 + +PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBUcmFuc2l0aW9uYWwv +L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== + +--===============2138658284696597373==-- +. +` + 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_NestedMultipartBase64(t *testing.T) { + email := `EHLO example.com + +MAIL FROM: test@mydomain.me +RCPT TO: ntfy-mytopic@ntfy.sh +DATA +Content-Type: multipart/mixed; boundary="===============2138658284696597373==" +MIME-Version: 1.0 +Subject: TrueNAS truenas.local: TrueNAS Test Message hostname: truenas.local +From: =?utf-8?q?Robbie?= +To: test@mydomain.me +Date: Thu, 16 Feb 2023 01:04:00 -0000 +Message-ID: + +This is a multi-part message in MIME format. +--===============2138658284696597373== +Content-Type: multipart/alternative; boundary="===============2233989480071754745==" +MIME-Version: 1.0 + +--===============2233989480071754745== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 + +VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSBmcm9tIFRydWVOQVMgQ09SRS4= + +--===============2233989480071754745== +Content-Type: text/html; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 + +PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBUcmFuc2l0aW9uYWwv +L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== + +--===============2233989480071754745==-- + +--===============2138658284696597373==-- +. +` + + 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") + } +*/ 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) {