diff --git a/README.md b/README.md index 263d687..72e6968 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -# smtp2shoutrrr +# smtp2Shoutrrr +A simple SMTP server that forwards incoming emails to a Shoutrrr service. + +## Development + +Run the server with: + +``` +go run main.go +``` + +Send a test email with: + +``` +go run ./cmd/sendmail/main.go diff --git a/cmd/sendmail/main.go b/cmd/sendmail/main.go new file mode 100644 index 0000000..8240473 --- /dev/null +++ b/cmd/sendmail/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "log" + "log/slog" + "net/smtp" + + "github.com/emersion/go-sasl" +) + +// The ANONYMOUS mechanism name. +const Anonymous = "ANONYMOUS" + +type anonymousClient struct { + Trace string +} + +func (c *anonymousClient) Start(si *smtp.ServerInfo) (mech string, ir []byte, err error) { + mech = Anonymous + ir = []byte(c.Trace) + return +} + +func (c *anonymousClient) Next(challenge []byte, b bool) (response []byte, err error) { + return nil, sasl.ErrUnexpectedServerChallenge +} + +// A client implementation of the ANONYMOUS authentication mechanism, as +// described in RFC 4505. +func NewAnonymousClient(trace string) smtp.Auth { + return &anonymousClient{trace} +} + +// The PLAIN mechanism name. +const Plain = "PLAIN" + +type plainClient struct { + Identity string + Username string + Password string +} + +func (a *plainClient) Start(si *smtp.ServerInfo) (mech string, ir []byte, err error) { + mech = "PLAIN" + ir = []byte(a.Identity + "\x00" + a.Username + "\x00" + a.Password) + return +} + +func (a *plainClient) Next(challenge []byte, b bool) (response []byte, err error) { + slog.Info("Next: %v", challenge) + return nil, nil +} + +// A client implementation of the PLAIN authentication mechanism, as described +// in RFC 4616. Authorization identity may be left blank to indicate that it is +// the same as the username. +func NewPlainClient(identity, username, password string) smtp.Auth { + return &plainClient{identity, username, password} +} + +func main() { + // hostname is used by PlainAuth to validate the TLS certificate. + hostname := "localhost" + auth := NewPlainClient("", "username", "password") + // auth := NewAnonymousClient("test") + recipients := []string{"test@test.com"} + msg := []byte("hello!") + from := "hello@localhost" + err := smtp.SendMail(hostname+":11025", auth, from, recipients, msg) + if err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..854f86a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.nakama.town/fmartingr/smtp2shoutrrr + +go 1.23.3 + +require ( + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 + github.com/emersion/go-smtp v0.21.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e12e95e --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..bc415f1 --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "errors" + "io" + "log" + "log/slog" + "net/http" + "time" + + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" +) + +// The Backend implements SMTP server methods. +type Backend struct{} + +// NewSession is called after client greeting (EHLO, HELO). +func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { + return &Session{}, nil +} + +// A Session is returned after successful login. +type Session struct{} + +// AuthMechanisms returns a slice of available auth mechanisms; only PLAIN is +// supported in this example. +func (s *Session) AuthMechanisms() []string { + return []string{sasl.Plain} +} + +// Auth is the handler for supported authenticators. +func (s *Session) Auth(mech string) (sasl.Server, error) { + return sasl.NewPlainServer(func(identity, username, password string) error { + if username != "username" || password != "password" { + return errors.New("Invalid username or password") + } + return nil + }), nil +} + +func (s *Session) Mail(from string, opts *smtp.MailOptions) error { + log.Println("Mail from:", from, opts) + return nil +} + +func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error { + log.Println("Rcpt to:", to) + return nil +} + +func (s *Session) Data(r io.Reader) error { + _, err := http.Post("https://ntfy.sh/fmartingr-dev", "text/plain", r) + if err != nil { + slog.Error("Error sending message:", slog.String("err", err.Error())) + } + return nil +} + +func (s *Session) Reset() {} + +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() { + be := &Backend{} + + s := 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 + + log.Println("Starting server at", s.Addr) + if err := s.ListenAndServe(); err != nil { + log.Fatal(err) + } +}