diff --git a/cmd/sendmail/main.go b/cmd/sendmail/main.go index 155481e..8240473 100644 --- a/cmd/sendmail/main.go +++ b/cmd/sendmail/main.go @@ -63,8 +63,8 @@ func main() { hostname := "localhost" auth := NewPlainClient("", "username", "password") // auth := NewAnonymousClient("test") - recipients := []string{"something@something.com", "caca@caca.com"} - msg := []byte("\r\nwithout title") + recipients := []string{"test@test.com"} + msg := []byte("hello!") from := "hello@localhost" err := smtp.SendMail(hostname+":11025", auth, from, recipients, msg) if err != nil { diff --git a/config.toml b/config.toml deleted file mode 100644 index 6591802..0000000 --- a/config.toml +++ /dev/null @@ -1,7 +0,0 @@ -[[Recipients]] -Addresses = ["something@something.com"] -Target = "ntfy://ntfy.sh/fmartingr-dev" - -[[Recipients]] -Addresses = ["caca@caca.com"] -Target = "ntfy://ntfy.sh/fmartingr-dev" diff --git a/go.mod b/go.mod index d8eee2b..854f86a 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,6 @@ module git.nakama.town/fmartingr/smtp2shoutrrr go 1.23.3 require ( - git.nakama.town/fmartingr/gotoolkit v0.0.0-00010101000000-000000000000 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.21.3 ) - -require ( - github.com/containrrr/shoutrrr v0.8.0 // indirect - github.com/fatih/color v1.15.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.22.0 // indirect -) - -replace git.nakama.town/fmartingr/gotoolkit => ../gotoolkit diff --git a/go.sum b/go.sum index cf6d65f..e12e95e 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,4 @@ -github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= -github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= 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.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go index ed11bee..bc415f1 100644 --- a/main.go +++ b/main.go @@ -1,142 +1,27 @@ package main import ( - "context" "errors" - "fmt" "io" + "log" "log/slog" - "net/mail" - "net/url" - "slices" - "strings" + "net/http" "time" - "git.nakama.town/fmartingr/gotoolkit/model" - "git.nakama.town/fmartingr/gotoolkit/service" - "github.com/containrrr/shoutrrr" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) -type ReceivedEmail struct { - Recipients []string - Msg *mail.Message - body string -} - -func (re *ReceivedEmail) String() string { - return fmt.Sprintf(`FROM: %s - TO: %s, - BODY: %s`, re.Msg.Header.Get("From"), re.Recipients, re.Body()) -} - -func (re *ReceivedEmail) Body() string { - if re.body == "" { - body, err := io.ReadAll(re.Msg.Body) - if err != nil { - slog.Error("failed to read email body", slog.String("err", err.Error())) - } - re.body = string(body) - } - - return re.body -} - -type Config struct { - Recipients []ConfigRecipient -} - -type ConfigRecipient struct { - Addresses []string // email addresses - target string // shoutrrr address - targetURL *url.URL -} - -func (cr ConfigRecipient) Target() *url.URL { - if cr.targetURL == nil { - var err error - cr.targetURL, err = url.Parse(cr.target) - if err != nil { - slog.Error("failed to parse shoutrrr target URL", slog.String("target", cr.target), slog.String("err", err.Error())) - } - } - return cr.targetURL -} - -var _ model.Server = (*smtpServer)(nil) - -type smtpServer struct { - backend *smtp.Server - config Config -} - -func (s *smtpServer) IsEnabled() bool { - return true -} - -func (s *smtpServer) Start(_ context.Context) error { - slog.Info("Started SMTP server", slog.String("addr", s.backend.Addr)) - return s.backend.ListenAndServe() -} - -func (s *smtpServer) Stop(ctx context.Context) error { - slog.Info("Stopping SMTP server") - return s.backend.Shutdown(ctx) -} - -func NewSMTPServer(backend *smtp.Server) model.Server { - return &smtpServer{ - backend: backend, - } -} - // The Backend implements SMTP server methods. -type Backend struct { - config *Config -} +type Backend struct{} // NewSession is called after client greeting (EHLO, HELO). func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { - return &Session{ - forwarderFunc: bkd.forwardEmail, - }, nil -} - -func (bkd *Backend) forwardEmail(email ReceivedEmail) error { - slog.Info("forwading message", slog.String("email", email.String())) - - for _, r := range bkd.config.Recipients { - for _, a := range email.Recipients { - if slices.Contains(r.Addresses, a) { - urlParams := url.Values{ - "title": {email.Msg.Header.Get("Subject")}, - } - - destinationURL := r.Target() - destinationURL.RawQuery = urlParams.Encode() - - if err := shoutrrr.Send(destinationURL.String(), email.Body()); err != nil { - slog.Error("Error sending message", slog.String("err", err.Error())) - continue - } - - // Already sent via this configuration, go to the next - break - } - } - } - - return nil + return &Session{}, nil } // A Session is returned after successful login. -type Session struct { - addresses []string - body string - - forwarderFunc func(ReceivedEmail) error -} +type Session struct{} // AuthMechanisms returns a slice of available auth mechanisms; only PLAIN is // supported in this example. @@ -155,32 +40,20 @@ func (s *Session) Auth(mech string) (sasl.Server, error) { } func (s *Session) Mail(from string, opts *smtp.MailOptions) error { - slog.Debug("Mail from", slog.String("from", from)) + log.Println("Mail from:", from, opts) return nil } func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error { - slog.Debug("Rcpt to", slog.String("to", to)) - s.addresses = append(s.addresses, to) + log.Println("Rcpt to:", to) return nil } func (s *Session) Data(r io.Reader) error { - msg, err := mail.ReadMessage(r) + _, err := http.Post("https://ntfy.sh/fmartingr-dev", "text/plain", r) if err != nil { - slog.Error("Error reading data", slog.String("err", err.Error())) - return fmt.Errorf("Error reading data: %w", err) + slog.Error("Error sending message:", slog.String("err", err.Error())) } - - slog.Info("Received email", slog.String("destination", strings.Join(s.addresses, ","))) - - if err := s.forwarderFunc(ReceivedEmail{ - Recipients: s.addresses, - Msg: msg, - }); err != nil { - slog.Error("Error forwarding email", slog.String("err", err.Error())) - } - return nil } @@ -190,48 +63,34 @@ func (s *Session) Logout() error { return nil } +// ExampleServer runs an example SMTP server. +// +// It can be tested manually with e.g. netcat: +// +// > netcat -C localhost 1025 +// EHLO localhost +// AUTH PLAIN +// AHVzZXJuYW1lAHBhc3N3b3Jk +// MAIL FROM: +// RCPT TO: +// DATA +// Hey <3 +// . func main() { - config := &Config{ - Recipients: []ConfigRecipient{{ - Addresses: []string{"test@fmartingr.com", "caca@caca.com"}, - target: "ntfy://ntfy.sh/fmartingr-dev", - }, { - Addresses: []string{"something@something.com"}, - target: "ntfy://ntfy.sh/fmartingr-dev", - }}, - } + be := &Backend{} - be := &Backend{ - config: config, - } + s := smtp.NewServer(be) - smtpBackend := smtp.NewServer(be) + s.Addr = "localhost:11025" + s.Domain = "localhost" + s.WriteTimeout = 10 * time.Second + s.ReadTimeout = 10 * time.Second + s.MaxMessageBytes = 1024 * 1024 + s.MaxRecipients = 50 + s.AllowInsecureAuth = true - smtpBackend.Addr = "localhost:11025" - smtpBackend.Domain = "localhost" - smtpBackend.WriteTimeout = 10 * time.Second - smtpBackend.ReadTimeout = 10 * time.Second - smtpBackend.MaxMessageBytes = 1024 * 1024 - smtpBackend.MaxRecipients = 50 - smtpBackend.AllowInsecureAuth = true - - ctx := context.Background() - smtpServer := NewSMTPServer(smtpBackend) - - svc, err := service.NewService([]model.Server{ - smtpServer, - }) - if err != nil { - slog.Error("Error creating service:", slog.String("err", err.Error())) - return - } - - if err := svc.Start(ctx); err != nil { - slog.Error("Error starting service:", slog.String("err", err.Error())) - return - } - - if err := svc.WaitStop(ctx); err != nil { - slog.Error("Error waiting for service interruption:", slog.String("err", err.Error())) + log.Println("Starting server at", s.Addr) + if err := s.ListenAndServe(); err != nil { + log.Fatal(err) } }