diff --git a/README.md b/README.md index 9f4eaec..5f4e069 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,12 @@ Simple smtp server library for GO. Each received message will call the handler, package main import ( - "github.com/dutchcoders/smtpd" - "os" + "context" "fmt" + "log" + "os" + + "github.com/dutchcoders/smtpd" ) func main() { @@ -19,8 +22,18 @@ func main() { return nil }) - addr := fmt.Sprintf(":%s", os.Getenv("PORT")) - smtpd.ListenAndServe(addr) + listener := smtpd.NewListener( + smtpd.ListenWithPort(os.Getenv("PORT")), + ) + + server, err := smtpd.New( + smtpd.WithListener(listener), + ) + if err != nil { + panic(err) + } + + log.Fatal(server.ListenAndServe(context.Background())) } ``` diff --git a/config.go b/config.go index a329fa8..86d922b 100644 --- a/config.go +++ b/config.go @@ -2,23 +2,41 @@ package smtpd import ( "fmt" + "strconv" ) type Config struct { Listeners []Listener } -func WithListener(l Listener) func(*Config) error { +//WithListener takes 1 or more Listener structs to serve on. +func WithListener(ll ...Listener) func(*Config) error { return func(cfg *Config) error { - switch { - case l.Address == "": - return fmt.Errorf("Required field Listener.Address is empty!") - case l.Port == "": - return fmt.Errorf("Required field Listener.Port is empty!") - case l.Mode == "": - l.Mode = "plain" + if len(ll) == 0 { + return fmt.Errorf("Got no listeners to configure.") } - cfg.Listeners = append(cfg.Listeners, l) + + for n, l := range ll { + if l.ID == "" { + l.ID = strconv.Itoa(n) + } + + if l.Mode == "" { + l.Mode = "plain" + } + + if l.Port == "" { + return fmt.Errorf("[%s] Required field \"Port\" is empty!", l.ID) + } + + if l.Mode == "tls" || l.Mode == "starttls" { + if l.TLSConfig == nil { + return fmt.Errorf("[%s] Mode 'tls/starttls' requires a tls config.", l.ID) + } + } + } + + cfg.Listeners = append(cfg.Listeners, ll...) return nil } } diff --git a/config_test.go b/config_test.go index 62cecc3..12defec 100644 --- a/config_test.go +++ b/config_test.go @@ -1,17 +1,66 @@ package smtpd -import "testing" +import ( + "crypto/tls" + "testing" +) + +func TestWithListenerTLSNoConfig(t *testing.T) { + l := Listener{ + Port: "8025", + Mode: "tls", + } + + err := WithListener(l)(&Config{}) + if err == nil { + t.Error("Mode 'tls' without tls config did not return an error.") + } + + l.Mode = "starttls" + + err = WithListener(l)(&Config{}) + if err == nil { + t.Error("Mode 'starttls' without tls config did not return an error.") + } +} + +func TestWithListenerTLSAndConfig(t *testing.T) { + l := Listener{ + Port: "8025", + TLSConfig: &tls.Config{}, + Mode: "tls", + } + + err := WithListener(l)(&Config{}) + if err != nil { + t.Errorf("Mode 'tls' with tls config gives error: %v", err) + } + + l.Mode = "starttls" + + err = WithListener(l)(&Config{}) + if err != nil { + t.Errorf("Mode 'starttls' with tls config gives error: %v", err) + } +} func TestWithListener(t *testing.T) { l := Listener{ - Address: "127.0.0.1", - Port: "8025", - Mode: "tls", + ID: "test", + Address: "127.0.0.1", + Port: "8025", + Mode: "tls", + Banner: func() string { return "test" }, + TLSConfig: &tls.Config{}, + Handler: DefaultServeMux, } cfg := &Config{} - _ = WithListener(l)(cfg) + err := WithListener(l)(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } got := cfg.Listeners @@ -30,6 +79,22 @@ func TestWithListener(t *testing.T) { if got[0].Mode != l.Mode { t.Errorf("Listener.Address got %s, want %s", got[0].Mode, l.Mode) } + + if got[0].ID != l.ID { + t.Errorf("Listener.ID got %s, want %s", got[0].ID, l.ID) + } + + if s := got[0].Banner(); s != "test" { + t.Errorf("Listener.Banner got %s, want %s", s, "test") + } + + if got[0].TLSConfig == nil { + t.Error("Listener.TLSConfig got ") + } + + if got[0].Handler != DefaultServeMux { + t.Error("Listener.Handler is not DefaultServeMux") + } } func TestWithListenerError(t *testing.T) { @@ -45,10 +110,7 @@ func TestWithListenerError(t *testing.T) { } func TestWithListenerDefaultMode(t *testing.T) { - l := Listener{ - Address: "127.0.0.1", - Port: "8025", - } + l := NewListener(ListenWithPort("25")) cfg := &Config{} diff --git a/listener.go b/listener.go index e0784fa..4372a41 100644 --- a/listener.go +++ b/listener.go @@ -3,25 +3,41 @@ package smtpd import "crypto/tls" type Listener struct { - ID string - Address string - Port string - Mode string //smtp modes: 'plain (25)', 'tls (465)' or 'starttls (587)' - Banner func() string + //ID optional text to identify the listener in the logs. + ID string + + //Address optional network address to listen on. Default: localhost + Address string + + //Port required port to listen on. + Port string + + //Mode optional smtp mode to use. + //smtp modes: 'plain (25)', 'tls (465)' or 'starttls (587)' + Mode string + + //Banner optional function returning the banner text shown to clients. + Banner func() string + + //TLSConfig optional tls configuration. + //note: mode 'tls' and 'starttls' require one, + // in mode 'plain' STARTTLS command will not be available without a config. TLSConfig *tls.Config - Handler Handler + + //Handler optional handler(s) for this listener. Default: DefaultHandler + Handler Handler } -func NewListener(options ...func(*Listener)) { - l := &Listener{ - ID: "-", +func NewListener(options ...func(*Listener)) Listener { + l := Listener{ Mode: "plain", Banner: func() string { return "DutchCoders SMTPd" }, } for _, opt := range options { - opt(l) + opt(&l) } + return l } func ListenWithID(id string) func(*Listener) { diff --git a/smtpd.go b/smtpd.go index f6443ea..d55f009 100644 --- a/smtpd.go +++ b/smtpd.go @@ -58,6 +58,8 @@ type smtpServer struct { var ErrServerClosed = errors.New("SMTPd Closed.") +//ListenAndServe starts serving smtp on the configured listeners. +//Always returns an error. func (s *Server) ListenAndServe(ctx context.Context) error { //lctx, cancel := context.WithCancel(ctx) //defer cancel()