list

server

This is the main dovel repository, it has the Go code to run dovel SMTP server.

curl -O https://dovel.email/server.tar.gz tar.gz

150bb62

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

Dockerfile

diff --git a/Dockerfile b/Dockerfile
old mode 100644
new mode 100755

LICENSE

diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755

README.md

diff --git a/README.md b/README.md
old mode 100644
new mode 100755

TODO.txt

diff --git a/TODO.txt b/TODO.txt
old mode 100644
new mode 100755

backend.go

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,

doc.go

diff --git a/doc.go b/doc.go
old mode 100644
new mode 100755

go.mod

diff --git a/go.mod b/go.mod
old mode 100644
new mode 100755

go.sum

diff --git a/go.sum b/go.sum
old mode 100644
new mode 100755

main.go

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

model.go

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