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