Fix encoding issues
parent
7cfe909644
commit
113053a9e3
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue