Fix encoding issues
This commit is contained in:
		
							parent
							
								
									7cfe909644
								
							
						
					
					
						commit
						113053a9e3
					
				
					 3 changed files with 213 additions and 7 deletions
				
			
		|  | @ -769,7 +769,7 @@ func (s *Server) runSMTPServer() error { | ||||||
| 	s.smtpServer.Domain = s.config.SMTPServerDomain | 	s.smtpServer.Domain = s.config.SMTPServerDomain | ||||||
| 	s.smtpServer.ReadTimeout = 10 * time.Second | 	s.smtpServer.ReadTimeout = 10 * time.Second | ||||||
| 	s.smtpServer.WriteTimeout = 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.MaxRecipients = 1 | ||||||
| 	s.smtpServer.AllowInsecureAuth = true | 	s.smtpServer.AllowInsecureAuth = true | ||||||
| 	return s.smtpServer.ListenAndServe() | 	return s.smtpServer.ListenAndServe() | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"github.com/emersion/go-smtp" | 	"github.com/emersion/go-smtp" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"mime" | ||||||
|  | 	"mime/multipart" | ||||||
| 	"net/mail" | 	"net/mail" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | @ -15,6 +17,7 @@ var ( | ||||||
| 	errInvalidAddress         = errors.New("invalid address") | 	errInvalidAddress         = errors.New("invalid address") | ||||||
| 	errInvalidTopic           = errors.New("invalid topic") | 	errInvalidTopic           = errors.New("invalid topic") | ||||||
| 	errTooManyRecipients      = errors.New("too many recipients") | 	errTooManyRecipients      = errors.New("too many recipients") | ||||||
|  | 	errUnsupportedContentType = errors.New("unsupported content type") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // smtpBackend implements SMTP server methods. | // smtpBackend implements SMTP server methods. | ||||||
|  | @ -94,6 +97,7 @@ func (s *smtpSession) Rcpt(to string) error { | ||||||
| 
 | 
 | ||||||
| func (s *smtpSession) Data(r io.Reader) error { | func (s *smtpSession) Data(r io.Reader) error { | ||||||
| 	return s.withFailCount(func() error { | 	return s.withFailCount(func() error { | ||||||
|  | 		conf := s.backend.config | ||||||
| 		b, err := io.ReadAll(r) // Protected by MaxMessageBytes | 		b, err := io.ReadAll(r) // Protected by MaxMessageBytes | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | @ -102,13 +106,21 @@ func (s *smtpSession) Data(r io.Reader) error { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		body, err := io.ReadAll(io.LimitReader(msg.Body, int64(s.backend.config.MessageLimit))) | 		body, err := readMailBody(msg) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			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") | 		subject := msg.Header.Get("Subject") | ||||||
| 		if subject != "" { | 		if subject != "" { | ||||||
|  | 			dec := mime.WordDecoder{} | ||||||
|  | 			subject, err := dec.DecodeHeader(subject) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 			m.Title = subject | 			m.Title = subject | ||||||
| 		} | 		} | ||||||
| 		if err := s.backend.sub(m); err != nil { | 		if err := s.backend.sub(m); err != nil { | ||||||
|  | @ -140,3 +152,39 @@ func (s *smtpSession) withFailCount(fn func() error) error { | ||||||
| 	} | 	} | ||||||
| 	return err | 	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
									
								
								server/smtp_server_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								server/smtp_server_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'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 | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue