diff --git a/go.mod b/go.mod index 918e9fc..fad88a4 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( firebase.google.com/go v3.13.0+incompatible github.com/BurntSushi/toml v0.4.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/emersion/go-smtp v0.15.0 github.com/mattn/go-sqlite3 v1.14.9 github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 github.com/stretchr/testify v1.7.0 @@ -26,6 +27,7 @@ require ( github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/envoyproxy/go-control-plane v0.10.1 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index c7b18e1..91718f4 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= +github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/server/mailserver.go b/server/mailserver.go new file mode 100644 index 0000000..08f2c19 --- /dev/null +++ b/server/mailserver.go @@ -0,0 +1,102 @@ +package server + +import ( + "bytes" + "errors" + "fmt" + "github.com/emersion/go-smtp" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/mail" + "strings" + "sync" +) + +// mailBackend implements SMTP server methods. +type mailBackend struct { + s *Server +} + +func (b *mailBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { + return &Session{s: b.s}, nil +} + +func (b *mailBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { + return &Session{s: b.s}, nil +} + +// Session is returned after EHLO. +type Session struct { + s *Server + from, to string + mu sync.Mutex +} + +func (s *Session) AuthPlain(username, password string) error { + return nil +} + +func (s *Session) Mail(from string, opts smtp.MailOptions) error { + s.mu.Lock() + defer s.mu.Unlock() + s.from = from + log.Println("Mail from:", from) + return nil +} + +func (s *Session) Rcpt(to string) error { + s.mu.Lock() + defer s.mu.Unlock() + s.to = to + log.Println("Rcpt to:", to) + return nil +} + +func (s *Session) Data(r io.Reader) error { + s.mu.Lock() + defer s.mu.Unlock() + b, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + log.Println("Data:", string(b)) + msg, err := mail.ReadMessage(bytes.NewReader(b)) + if err != nil { + return err + } + body, err := io.ReadAll(msg.Body) + if err != nil { + return err + } + topic := strings.TrimSuffix(s.to, "@ntfy.sh") + url := fmt.Sprintf("%s/%s", s.s.config.BaseURL, topic) + req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) + if err != nil { + return err + } + subject := msg.Header.Get("Subject") + if subject != "" { + req.Header.Set("Title", subject) + } + rr := httptest.NewRecorder() + s.s.handle(rr, req) + if rr.Code != http.StatusOK { + return errors.New("error: " + rr.Body.String()) + } + return nil +} + +func (s *Session) Reset() { + s.mu.Lock() + s.from = "" + s.to = "" + s.mu.Unlock() +} + +func (s *Session) Logout() error { + return nil +} diff --git a/server/server.go b/server/server.go index 78715d2..c2f8034 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( firebase "firebase.google.com/go" "firebase.google.com/go/messaging" "fmt" + "github.com/emersion/go-smtp" "google.golang.org/api/option" "heckel.io/ntfy/util" "html/template" @@ -238,10 +239,16 @@ func (s *Server) Run() error { errChan <- s.httpsServer.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile) }() } + if true { + go func() { + errChan <- s.mailserver() + }() + } s.mu.Unlock() go s.runManager() go s.runAtSender() go s.runFirebaseKeepliver() + return <-errChan } @@ -722,6 +729,21 @@ func (s *Server) updateStatsAndPrune() { s.messages, len(s.topics), subscribers, messages, len(s.visitors)) } +func (s *Server) mailserver() error { + ms := smtp.NewServer(&mailBackend{s}) + + ms.Addr = ":1025" + ms.Domain = "localhost" + ms.ReadTimeout = 10 * time.Second + ms.WriteTimeout = 10 * time.Second + ms.MaxMessageBytes = 1024 * 1024 + ms.MaxRecipients = 50 + ms.AllowInsecureAuth = true + + log.Println("Starting server at", ms.Addr) + return ms.ListenAndServe() +} + func (s *Server) runManager() { for { select {