This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: brian (brian@myr.sh)
Date: Thu Apr 18 18:40:42 2024 -0300
Parent: 04f6488
Improved handler logic - Changed some log levels - Updated docs - Removed dep on external vault - Added new configuration options
diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755
diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755
diff --git a/README.md b/README.md old mode 100644 new mode 100755
diff --git a/TODO.txt b/TODO.txt old mode 100644 new mode 100755
diff --git a/backend.go b/backend.go old mode 100644 new mode 100755 index b9a80c1..a9c4d4a --- a/backend.go +++ b/backend.go @@ -79,11 +79,19 @@ func (s *Session) Data(raw io.Reader) error { return err } - // sending email - dom := strings.Split(s.from, "@")[1] - if dom == cfg.Domain { + op := "receive" + if dom := strings.Split(s.from, "@"); dom[1] == cfg.Domain { + op = "send" + } + + hs := []string{} + switch op { + case "send": + slog.Info("operation is send") + // check auth if s.user.Name == "" { + slog.Info("send error: empty name") return smtp.ErrAuthRequired } err = s.Send(s.from, tos, strings.NewReader(string(cont))) @@ -91,10 +99,22 @@ func (s *Session) Data(raw io.Reader) error { slog.Error("send error", "msg", err.Error()) return err } + hs = []string{path.Join(configPath, "hooks", "send-"+cfg.Domain)} + case "receive": + slog.Info("operation is receive") + for _, to := range tos { + domain := strings.Split(to.Address, "@")[1] + hs = append(hs, path.Join(configPath, "hooks", "receive-"+domain)) + } + } - // running handlers is optional - h := path.Join(configPath, "hooks", "send-"+cfg.Domain) - if f, err := os.Lstat(h); err == nil { + // running handlers is optional + for _, h := range hs { + slog.Info("running handler", "file", h) + if f, err := os.Lstat(h); err != nil { + slog.Warn("handler error", "err", err.Error()) + continue + } else { if !f.Mode().IsRegular() { h, err = os.Readlink(h) if err != nil { @@ -114,32 +134,6 @@ func (s *Session) Data(raw io.Reader) error { } } - // receiving email - for _, to := range tos { - domain := strings.Split(to.Address, "@")[1] - - h := path.Join(configPath, "hooks", "receive-"+domain) - if f, err := os.Lstat(h); err != nil { - slog.Error("lstat error", "domain", domain, "error", err.Error()) - break - } else if !f.Mode().IsRegular() { - h, err = os.Readlink(h) - if err != nil { - slog.Error("read link", "domain", domain, "error", err.Error()) - } - } - - c := exec.Command(h) - c.Dir = path.Join(configPath, "hooks") - c.Stdin = strings.NewReader(string(mess)) - out := bytes.Buffer{} - c.Stdout = &out - if err = c.Run(); err != nil { - slog.Error("run script", "error", err.Error(), "output", out.String()) - break - } - } - return err } @@ -183,7 +177,7 @@ func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error { slog.Debug("checking wkd key") key, _ := wkd.Discover(to.Address) if key != nil { - slog.Debug("found wkd key") + slog.Info("found WKD key", "address", to.Address) enc := bytes.Buffer{} c, err := openpgp.Encrypt(&enc, key, key[0], nil, nil) if err != nil { @@ -210,7 +204,7 @@ func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error { slog.Debug("dkim check") res := bytes.Buffer{} if keyPath := s.user.PrivateKey; keyPath != "" { - slog.Debug("user has dkim key") + slog.Info("user has dkim key") keyData, err := os.ReadFile(keyPath) if err != nil { @@ -232,7 +226,7 @@ func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error { slog.Error("failed to sign body", "err", err) } } else { - slog.Debug("no dkim key") + slog.Info("no dkim key") io.Copy(&res, bytes.NewReader(body)) } @@ -242,7 +236,7 @@ func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error { } server := mxs[0].Host + ":smtp" - slog.Debug("sending", "host", server, "from", from, "to", tos) + slog.Info("sending", "host", server, "from", from, "to", tos) err = smtp.SendMail( server, nil,
diff --git a/doc.go b/doc.go old mode 100644 new mode 100755
diff --git a/go.mod b/go.mod old mode 100644 new mode 100755
diff --git a/go.sum b/go.sum old mode 100644 new mode 100755
diff --git a/main.go b/main.go old mode 100644 new mode 100755 index e57d5a4..8e62905 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ import ( var ( cfg = Config{} configPath string - v store + v Vault ) func main() { @@ -29,7 +29,7 @@ func main() { var err error configPath, err = os.UserConfigDir() if err != nil { - slog.Error(err.Error(), "using ~/.config/dovel/config.json") + slog.Warn(err.Error(), "details", "using ~/.config/dovel/config.json") configPath = "~/.config" } configPath = path.Join(configPath, "dovel") @@ -41,33 +41,33 @@ func main() { json.NewDecoder(configFile).Decode(&cfg) slog.Debug("config loaded", "config", cfg) - tlsCfg := &tls.Config{} - if cfg.Certificate != "" { - slog.Debug("loading certs", "cert", cfg.Certificate, "key", cfg.PrivateKey) - c, err := tls.LoadX509KeyPair(cfg.Certificate, cfg.PrivateKey) + if cfg.VaultFile != "" { + slog.Info("loading users", "file", cfg.VaultFile) + v, err = NewStore(cfg.VaultFile) if err != nil { - panic(err) + slog.Warn("failed to load users", "error", err.Error()) } - tlsCfg.Certificates = []tls.Certificate{c} } s := smtp.NewServer(backend{}) s.Addr = ":" + cfg.Port s.Domain = cfg.Domain - s.ReadTimeout = 10 * time.Second - s.WriteTimeout = 10 * time.Second - s.MaxMessageBytes = 1024 * 1024 - s.MaxRecipients = 5 - s.AllowInsecureAuth = true + s.ReadTimeout = time.Duration(cfg.ReadTimeout) * time.Second + s.WriteTimeout = time.Duration(cfg.WriteTimeout) * time.Second + s.MaxMessageBytes = cfg.MaxMessageBytes + s.MaxRecipients = cfg.MaxRecipients + s.AllowInsecureAuth = cfg.AllowInsecureAuth - if len(tlsCfg.Certificates) > 0 { - s.TLSConfig = tlsCfg - slog.Info("server started", "tls", tlsCfg, "port", cfg.Port) - err = s.ListenAndServeTLS() - } else { - slog.Info("server started", "port", cfg.Port) - err = s.ListenAndServe() + if cfg.Certificate != "" { + slog.Debug("loading certs", "cert", cfg.Certificate, "key", cfg.PrivateKey) + c, err := tls.LoadX509KeyPair(cfg.Certificate, cfg.PrivateKey) + if err != nil { + panic(err) + } + s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{c}} } + + err = s.ListenAndServe() if err != nil { panic(err) }
diff --git a/model.go b/model.go old mode 100644 new mode 100755 index 6bb2bc3..7a70155 --- a/model.go +++ b/model.go @@ -1,5 +1,10 @@ package main +import ( + "encoding/json" + "os" +) + // Config is used to configure your email server. // Port is used to specify which port the server should listen // to connections, a typicall value is 2525. @@ -9,10 +14,16 @@ package main // In order to use TLS connections Certficate and PrivateKey fields // must have valid path to pem encoded keys. type Config struct { - Port string - Domain string - Certificate string - PrivateKey string + Port string + Domain string + Certificate string + PrivateKey string + VaultFile string + ReadTimeout int + WriteTimeout int + MaxMessageBytes int64 + MaxRecipients int + AllowInsecureAuth bool } // Vault interface gives validation and user fetching. @@ -35,6 +46,17 @@ type store struct { users []User } +func NewStore(path string) (Vault, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + s := store{users: []User{}} + json.NewDecoder(f).Decode(&s.users) + return s, err +} + func (s store) Validate(n, p string) bool { u := s.GetUser(n) return u.Password == p