ntfy/server/smtp_server.go

252 lines
6.2 KiB
Go
Raw Normal View History

2021-12-27 18:39:28 +03:00
package server
import (
"bytes"
"errors"
2022-06-01 03:38:56 +03:00
"fmt"
2021-12-27 18:39:28 +03:00
"github.com/emersion/go-smtp"
"io"
2021-12-28 03:26:20 +03:00
"mime"
"mime/multipart"
2022-06-02 06:24:44 +03:00
"net"
2022-06-01 03:38:56 +03:00
"net/http"
"net/http/httptest"
2021-12-27 18:39:28 +03:00
"net/mail"
"strings"
"sync"
)
2021-12-28 00:06:40 +03:00
var (
2021-12-28 03:26:20 +03:00
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")
2021-12-28 00:06:40 +03:00
)
2021-12-27 18:39:28 +03:00
// smtpBackend implements SMTP server methods.
type smtpBackend struct {
2021-12-28 00:06:40 +03:00
config *Config
2022-06-01 03:38:56 +03:00
handler func(http.ResponseWriter, *http.Request)
2021-12-28 00:06:40 +03:00
success int64
failure int64
mu sync.Mutex
2021-12-27 18:39:28 +03:00
}
2022-06-01 03:38:56 +03:00
func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend {
2021-12-27 18:39:28 +03:00
return &smtpBackend{
2022-06-01 03:38:56 +03:00
config: conf,
handler: handler,
2021-12-27 18:39:28 +03:00
}
}
2023-02-04 06:21:50 +03:00
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, _ string) (smtp.Session, error) {
logem(state).Debug("Incoming mail, login with user %s", username)
2022-06-02 06:24:44 +03:00
return &smtpSession{backend: b, state: state}, nil
2021-12-27 18:39:28 +03:00
}
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
2023-02-04 06:21:50 +03:00
logem(state).Debug("Incoming mail, anonymous login")
2022-06-02 06:24:44 +03:00
return &smtpSession{backend: b, state: state}, nil
2021-12-28 00:06:40 +03:00
}
2022-06-02 06:24:44 +03:00
func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
2021-12-28 00:06:40 +03:00
b.mu.Lock()
defer b.mu.Unlock()
2022-06-02 06:24:44 +03:00
return b.success + b.failure, b.success, b.failure
2021-12-27 18:39:28 +03:00
}
// smtpSession is returned after EHLO.
type smtpSession struct {
2022-06-02 06:24:44 +03:00
backend *smtpBackend
state *smtp.ConnectionState
topic string
mu sync.Mutex
2021-12-27 18:39:28 +03:00
}
func (s *smtpSession) AuthPlain(username, password string) error {
2023-02-04 06:21:50 +03:00
logem(s.state).Debug("AUTH PLAIN (with username %s)", username)
2021-12-27 18:39:28 +03:00
return nil
}
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
2023-02-05 05:26:01 +03:00
logem(s.state).Debug("MAIL FROM: %s (with options: %#v)", from, opts)
2021-12-27 18:39:28 +03:00
return nil
}
func (s *smtpSession) Rcpt(to string) error {
2023-02-04 06:21:50 +03:00
logem(s.state).Debug("RCPT TO: %s", to)
2021-12-28 00:06:40 +03:00
return s.withFailCount(func() error {
conf := s.backend.config
addressList, err := mail.ParseAddressList(to)
if err != nil {
return err
} else if len(addressList) != 1 {
return errTooManyRecipients
}
to = addressList[0].Address
if !strings.HasSuffix(to, "@"+conf.SMTPServerDomain) {
return errInvalidDomain
}
to = strings.TrimSuffix(to, "@"+conf.SMTPServerDomain)
if conf.SMTPServerAddrPrefix != "" {
if !strings.HasPrefix(to, conf.SMTPServerAddrPrefix) {
return errInvalidAddress
}
to = strings.TrimPrefix(to, conf.SMTPServerAddrPrefix)
}
if !topicRegex.MatchString(to) {
return errInvalidTopic
}
s.mu.Lock()
s.topic = to
s.mu.Unlock()
return nil
})
2021-12-27 18:39:28 +03:00
}
func (s *smtpSession) Data(r io.Reader) error {
2021-12-28 00:06:40 +03:00
return s.withFailCount(func() error {
2021-12-28 03:26:20 +03:00
conf := s.backend.config
2021-12-28 00:06:40 +03:00
b, err := io.ReadAll(r) // Protected by MaxMessageBytes
if err != nil {
return err
}
2023-02-04 06:21:50 +03:00
ev := logem(s.state).Tag(tagSMTP)
if ev.IsTrace() {
ev.Field("smtp_data", string(b)).Trace("DATA")
} else if ev.IsDebug() {
ev.Debug("DATA: %d byte(s)", len(b))
2022-06-02 06:24:44 +03:00
}
2021-12-28 00:06:40 +03:00
msg, err := mail.ReadMessage(bytes.NewReader(b))
if err != nil {
return err
}
2021-12-28 03:26:20 +03:00
body, err := readMailBody(msg)
2021-12-28 00:06:40 +03:00
if err != nil {
return err
}
body = strings.TrimSpace(body)
2021-12-28 03:26:20 +03:00
if len(body) > conf.MessageLimit {
body = body[:conf.MessageLimit]
}
m := newDefaultMessage(s.topic, body)
subject := strings.TrimSpace(msg.Header.Get("Subject"))
2021-12-28 00:06:40 +03:00
if subject != "" {
2021-12-28 03:26:20 +03:00
dec := mime.WordDecoder{}
subject, err := dec.DecodeHeader(subject)
if err != nil {
return err
}
2021-12-28 00:06:40 +03:00
m.Title = subject
}
if m.Title != "" && m.Message == "" {
m.Message = m.Title // Flip them, this makes more sense
m.Title = ""
}
2022-06-01 03:38:56 +03:00
if err := s.publishMessage(m); err != nil {
2021-12-28 00:06:40 +03:00
return err
}
s.backend.mu.Lock()
s.backend.success++
s.backend.mu.Unlock()
return nil
})
2021-12-27 18:39:28 +03:00
}
2022-06-01 03:38:56 +03:00
func (s *smtpSession) publishMessage(m *message) error {
2022-06-02 06:24:44 +03:00
// Extract remote address (for rate limiting)
remoteAddr, _, err := net.SplitHostPort(s.state.RemoteAddr.String())
if err != nil {
remoteAddr = s.state.RemoteAddr.String()
}
// Call HTTP handler with fake HTTP request
2022-06-01 03:38:56 +03:00
url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic)
2022-06-02 06:24:44 +03:00
req, err := http.NewRequest("POST", url, strings.NewReader(m.Message))
req.RequestURI = "/" + m.Topic // just for the logs
req.RemoteAddr = remoteAddr // rate limiting!!
req.Header.Set("X-Forwarded-For", remoteAddr)
2022-06-01 03:38:56 +03:00
if err != nil {
return err
}
if m.Title != "" {
req.Header.Set("Title", m.Title)
}
rr := httptest.NewRecorder()
s.backend.handler(rr, req)
if rr.Code != http.StatusOK {
return errors.New("error: " + rr.Body.String())
}
return nil
}
2021-12-27 18:39:28 +03:00
func (s *smtpSession) Reset() {
s.mu.Lock()
2021-12-28 00:06:40 +03:00
s.topic = ""
2021-12-27 18:39:28 +03:00
s.mu.Unlock()
}
func (s *smtpSession) Logout() error {
return nil
}
2021-12-28 00:06:40 +03:00
func (s *smtpSession) withFailCount(fn func() error) error {
err := fn()
s.backend.mu.Lock()
defer s.backend.mu.Unlock()
if err != nil {
2022-06-02 06:24:44 +03:00
// Almost all of these errors are parse errors, and user input errors.
// We do not want to spam the log with WARN messages.
2023-02-04 06:21:50 +03:00
logem(s.state).Err(err).Debug("Incoming mail error")
2021-12-28 00:06:40 +03:00
s.backend.failure++
}
return err
}
2021-12-28 03:26:20 +03:00
func readMailBody(msg *mail.Message) (string, error) {
if msg.Header.Get("Content-Type") == "" {
return readPlainTextMailBody(msg)
}
2021-12-28 03:26:20 +03:00
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/") {
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 {
part, err := mr.NextPart()
if err != nil { // may be io.EOF
return "", err
}
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
2021-12-28 03:26:20 +03:00
if err != nil {
return "", err
}
if partContentType != "text/plain" {
continue
}
body, err := io.ReadAll(part)
if err != nil {
return "", err
2021-12-28 03:26:20 +03:00
}
return string(body), nil
2021-12-28 03:26:20 +03:00
}
}