This commit is contained in:
Felipe M 2024-11-13 18:41:37 +01:00
parent 8ec33d6fd7
commit 14792a4234
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
5 changed files with 196 additions and 1 deletions

View file

@ -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

73
cmd/sendmail/main.go Normal file
View file

@ -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)
}
}

8
go.mod Normal file
View file

@ -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
)

4
go.sum Normal file
View file

@ -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=

96
main.go Normal file
View file

@ -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:<root@nsa.gov>
// RCPT TO:<root@gchq.gov.uk>
// 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)
}
}