Browse Source

Fix encoding issues

Philipp Heckel 3 years ago
parent
commit
113053a9e3
3 changed files with 213 additions and 7 deletions
  1. 1 1
      server/server.go
  2. 54 6
      server/smtp_server.go
  3. 158 0
      server/smtp_server_test.go

+ 1 - 1
server/server.go

@@ -769,7 +769,7 @@ func (s *Server) runSMTPServer() error {
 	s.smtpServer.Domain = s.config.SMTPServerDomain
 	s.smtpServer.ReadTimeout = 10 * time.Second
 	s.smtpServer.WriteTimeout = 10 * time.Second
-	s.smtpServer.MaxMessageBytes = 2 * s.config.MessageLimit
+	s.smtpServer.MaxMessageBytes = 1024 * 1024 // Must be much larger than message size (headers, multipart, etc.)
 	s.smtpServer.MaxRecipients = 1
 	s.smtpServer.AllowInsecureAuth = true
 	return s.smtpServer.ListenAndServe()

+ 54 - 6
server/smtp_server.go

@@ -5,16 +5,19 @@ import (
 	"errors"
 	"github.com/emersion/go-smtp"
 	"io"
+	"mime"
+	"mime/multipart"
 	"net/mail"
 	"strings"
 	"sync"
 )
 
 var (
-	errInvalidDomain     = errors.New("invalid domain")
-	errInvalidAddress    = errors.New("invalid address")
-	errInvalidTopic      = errors.New("invalid topic")
-	errTooManyRecipients = errors.New("too many recipients")
+	errInvalidDomain          = errors.New("invalid domain")
+	errInvalidAddress         = errors.New("invalid address")
+	errInvalidTopic           = errors.New("invalid topic")
+	errTooManyRecipients      = errors.New("too many recipients")
+	errUnsupportedContentType = errors.New("unsupported content type")
 )
 
 // smtpBackend implements SMTP server methods.
@@ -94,6 +97,7 @@ func (s *smtpSession) Rcpt(to string) error {
 
 func (s *smtpSession) Data(r io.Reader) error {
 	return s.withFailCount(func() error {
+		conf := s.backend.config
 		b, err := io.ReadAll(r) // Protected by MaxMessageBytes
 		if err != nil {
 			return err
@@ -102,13 +106,21 @@ func (s *smtpSession) Data(r io.Reader) error {
 		if err != nil {
 			return err
 		}
-		body, err := io.ReadAll(io.LimitReader(msg.Body, int64(s.backend.config.MessageLimit)))
+		body, err := readMailBody(msg)
 		if err != nil {
 			return err
 		}
-		m := newDefaultMessage(s.topic, string(body))
+		if len(body) > conf.MessageLimit {
+			body = body[:conf.MessageLimit]
+		}
+		m := newDefaultMessage(s.topic, body)
 		subject := msg.Header.Get("Subject")
 		if subject != "" {
+			dec := mime.WordDecoder{}
+			subject, err := dec.DecodeHeader(subject)
+			if err != nil {
+				return err
+			}
 			m.Title = subject
 		}
 		if err := s.backend.sub(m); err != nil {
@@ -140,3 +152,39 @@ func (s *smtpSession) withFailCount(fn func() error) error {
 	}
 	return err
 }
+
+func readMailBody(msg *mail.Message) (string, error) {
+	contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
+	if err != nil {
+		return "", err
+	}
+	if contentType == "text/plain" {
+		body, err := io.ReadAll(msg.Body)
+		if err != nil {
+			return "", err
+		}
+		return string(body), nil
+	}
+	if strings.HasPrefix(contentType, "multipart/") {
+		mr := multipart.NewReader(msg.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"))
+			if err != nil {
+				return "", err
+			}
+			if partContentType != "text/plain" {
+				continue
+			}
+			body, err := io.ReadAll(part)
+			if err != nil {
+				return "", err
+			}
+			return string(body), nil
+		}
+	}
+	return "", errUnsupportedContentType
+}

+ 158 - 0
server/smtp_server_test.go

@@ -0,0 +1,158 @@
+package server
+
+import (
+	"github.com/emersion/go-smtp"
+	"github.com/stretchr/testify/require"
+	"strings"
+	"testing"
+)
+
+func TestSmtpBackend_Multipart(t *testing.T) {
+	email := `MIME-Version: 1.0
+Date: Tue, 28 Dec 2021 00:30:10 +0100
+Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
+Subject: and one more
+From: Phil <phil@example.com>
+To: ntfy-mytopic@ntfy.sh
+Content-Type: multipart/alternative; boundary="000000000000f3320b05d42915c9"
+
+--000000000000f3320b05d42915c9
+Content-Type: text/plain; charset="UTF-8"
+
+what's up
+
+--000000000000f3320b05d42915c9
+Content-Type: text/html; charset="UTF-8"
+
+<div dir="ltr">what&#39;s up<br clear="all"><div><br></div></div>
+
+--000000000000f3320b05d42915c9--`
+	_, backend := newTestBackend(t, func(m *message) error {
+		require.Equal(t, "mytopic", m.Topic)
+		require.Equal(t, "and one more", m.Title)
+		require.Equal(t, "what's up\n", m.Message)
+		return nil
+	})
+	session, _ := backend.AnonymousLogin(nil)
+	require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
+	require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
+	require.Nil(t, session.Data(strings.NewReader(email)))
+}
+
+func TestSmtpBackend_Plaintext(t *testing.T) {
+	email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
+Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
+Subject: and one more
+From: Phil <phil@example.com>
+To: mytopic@ntfy.sh
+Content-Type: text/plain; charset="UTF-8"
+
+what's up
+`
+	conf, backend := newTestBackend(t, func(m *message) error {
+		require.Equal(t, "mytopic", m.Topic)
+		require.Equal(t, "and one more", m.Title)
+		require.Equal(t, "what's up\n", m.Message)
+		return nil
+	})
+	conf.SMTPServerAddrPrefix = ""
+	session, _ := backend.AnonymousLogin(nil)
+	require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
+	require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
+	require.Nil(t, session.Data(strings.NewReader(email)))
+}
+
+func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
+	email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
+Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
+From: Phil <phil@example.com>
+To: ntfy-mytopic@ntfy.sh
+Content-Type: text/plain; charset="UTF-8"
+
+what's up
+`
+	_, backend := newTestBackend(t, func(m *message) error {
+		require.Equal(t, "Three santas ๐ŸŽ…๐ŸŽ…๐ŸŽ…", m.Title)
+		return nil
+	})
+	session, _ := backend.AnonymousLogin(nil)
+	require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
+	require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
+	require.Nil(t, session.Data(strings.NewReader(email)))
+}
+
+func TestSmtpBackend_Plaintext_TooLongTruncate(t *testing.T) {
+	email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
+Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
+Subject: and one more
+From: Phil <phil@example.com>
+To: mytopic@ntfy.sh
+Content-Type: text/plain; charset="UTF-8"
+
+you know this is a string.
+it's a long string. 
+it's supposed to be longer than the max message length
+which is 512 bytes,
+which some people say is too short
+but it kinda makes sense when you look at what it looks like one a phone
+heck this wasn't even half of it so far.
+so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+that should do it
+`
+	conf, backend := newTestBackend(t, func(m *message) error {
+		expected := `you know this is a string.
+it's a long string. 
+it's supposed to be longer than the max message length
+which is 512 bytes,
+which some people say is too short
+but it kinda makes sense when you look at what it looks like one a phone
+heck this wasn't even half of it so far.
+so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+and with `
+		require.Equal(t, expected, m.Message)
+		return nil
+	})
+	conf.SMTPServerAddrPrefix = ""
+	session, _ := backend.AnonymousLogin(nil)
+	require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
+	require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
+	require.Nil(t, session.Data(strings.NewReader(email)))
+}
+
+func TestSmtpBackend_Unsupported(t *testing.T) {
+	email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
+Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
+Subject: and one more
+From: Phil <phil@example.com>
+To: mytopic@ntfy.sh
+Content-Type: text/SOMETHINGELSE
+
+what's up
+`
+	conf, backend := newTestBackend(t, func(m *message) error {
+		return nil
+	})
+	conf.SMTPServerAddrPrefix = ""
+	session, _ := backend.Login(nil, "user", "pass")
+	require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
+	require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
+	require.Equal(t, errUnsupportedContentType, session.Data(strings.NewReader(email)))
+}
+
+func newTestBackend(t *testing.T, sub subscriber) (*Config, *smtpBackend) {
+	conf := newTestConfig(t)
+	conf.SMTPServerListen = ":25"
+	conf.SMTPServerDomain = "ntfy.sh"
+	conf.SMTPServerAddrPrefix = "ntfy-"
+	backend := newMailBackend(conf, sub)
+	return conf, backend
+}