This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Wed Sep 6 19:28:42 2023 -0300
Parent: cc638a4
Improved handler model
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/cmd/dovel/backend.go b/cmd/dovel/backend.go new file mode 100644 index 0000000..be441ac --- /dev/null +++ b/cmd/dovel/backend.go @@ -0,0 +1,200 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "net/mail" + "net/textproto" + "os" + "os/exec" + "path" + "strings" + "time" + + "git.derelict.garden/dovel/email/util/wkd" + "github.com/ProtonMail/gopenpgp/v2/helper" + "github.com/emersion/go-mbox" + "github.com/emersion/go-msgauth/dkim" + "github.com/emersion/go-smtp" +) + +// A Session is returned after EHLO. +type Session struct { + user string + from string + tos []string +} + +func (s *Session) AuthPlain(username, password string) error { + if !v.Validate(username, password) { + return fmt.Errorf("user not authorized") + } + s.user = username + return nil +} + +func (s *Session) Mail(from string, opts *smtp.MailOptions) error { + println("Mail from:", from) + s.from = from + return nil +} + +func (s *Session) Rcpt(to string) error { + println("Rcpt to:", to) + s.tos = append(s.tos, to) + return nil +} + +func (s *Session) Data(raw io.Reader) error { + cont, err := io.ReadAll(raw) + if err != nil { + return err + } + + w := bytes.Buffer{} + box := mbox.NewWriter(&w) + boxW, err := box.CreateMessage(s.from, time.Now()) + if err != nil { + return err + } + boxW.Write(cont) + box.Close() + + mess, err := io.ReadAll(&w) + if err != nil { + return err + } + + // sending email + from, err := mail.ParseAddress(s.from) + if err != nil { + println("parse from", err.Error()) + return err + } + domain := strings.Split(from.Address, "@")[1] + h := path.Join(configPath, "send-"+domain) + if _, err := os.Stat(h); err == nil { + err := s.Send(s.from, s.tos, strings.NewReader(string(cont))) + if err != nil { + return err + } + c := exec.Command(h) + c.Stdin = strings.NewReader(string(mess)) + if err := c.Run(); err != nil { + return err + } + } + + // receiving email + for _, to := range s.tos { + toAddr, err := mail.ParseAddress(to) + if err != nil { + println("parse to", err.Error()) + return err + } + domain := strings.Split(toAddr.Address, "@")[1] + + h := path.Join(configPath, "receive-"+domain) + if _, err := os.Stat(h); err != nil { + println(domain, "receive error:", err.Error()) + continue + } + + c := exec.Command(h) + c.Stdin = strings.NewReader(string(mess)) + if err := c.Run(); err != nil { + return err + } + + } + + return nil +} + +func (s *Session) Reset() {} + +func (s *Session) Logout() error { + println("logged out") + return nil +} + +func (s *Session) Send(from string, tos []string, raw io.Reader) error { + email, err := mail.ReadMessage(raw) + if err != nil { + return err + } + + body, err := ioutil.ReadAll(email.Body) + if err != nil { + return err + } + for _, to := range tos { + msg := string(body) + + // dns mx for email + addr := strings.Split(to, "@") + mxs, err := net.LookupMX(addr[1]) + if err != nil { + return err + } + if len(mxs) == 0 { + return err + } + + key, err := wkd.FetchPGPKey(addr[0], addr[1]) + if err != nil { + return err + } + if key != "" { + email.Header["Content-Type"] = []string{"application/pgp-encrypted"} + msg, err = helper.EncryptMessageArmored(key, msg) + if err != nil { + return err + } + } + + payload := bytes.Buffer{} + writer := textproto.NewWriter(bufio.NewWriter(&payload)) + for k, v := range email.Header { + writer.PrintfLine("%s: %s", k, strings.Join(v, ", ")) + } + writer.PrintfLine("") + payload.Write([]byte(msg)) + + // dkim + res := bytes.Buffer{} + options := &dkim.SignOptions{ + Domain: domain, + Selector: "dkim", + Signer: v.GetUser(s.user).PrivateKey, + } + err = dkim.Sign(&res, &payload, options) + if err != nil { + println("failed to sign body:", err.Error()) + } + + server := mxs[0].Host + ":smtp" + err = smtp.SendMail( + server, + nil, + from, + tos, + &res, + ) + if err != nil { + return err + } + + } + return nil +} + +type backend struct{} + +func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { + return &Session{tos: []string{}}, nil +}
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go index 828d69e..b647637 100644 --- a/cmd/dovel/main.go +++ b/cmd/dovel/main.go @@ -4,18 +4,28 @@ import ( "encoding/json" "os" "path" + "time" - "git.derelict.garden/dovel/email" + "git.derelict.garden/bryon/vault" "git.derelict.garden/dovel/email/model" + "github.com/emersion/go-smtp" +) + +var ( + domain string + configPath string + v vault.Vault[model.WebUser] ) func main() { - configPath, err := os.UserConfigDir() + var err error + configPath, err = os.UserConfigDir() if err != nil { println(err, "using ~/.config/dovel/config.json") configPath = "~/.config" } - configFile, err := os.Open(path.Join(configPath, "dovel", "config.json")) + configPath = path.Join(configPath, "dovel") + configFile, err := os.Open(path.Join(configPath, "config.json")) if err != nil { panic(err) } @@ -23,7 +33,21 @@ func main() { cfg := model.Config{} json.NewDecoder(configFile).Decode(&cfg) - if err := email.Start(cfg); err != nil { + v, err = vault.NewJSONPlainTextVault[model.WebUser](cfg.VaultFile) + if err != nil { + panic(err) + } + + 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 = 2 + s.AllowInsecureAuth = true + + if err := s.ListenAndServe(); err != nil { panic(err) } }
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/go.mod b/go.mod index eba4c70..8e77281 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module git.derelict.garden/dovel/email go 1.19 require ( - blmayer.dev/x/vault v0.2.0 git.derelict.garden/bryon/vault v0.3.0 github.com/OfimaticSRL/parsemail v0.0.0-20230215211201-e1c318cd177f github.com/ProtonMail/gopenpgp/v2 v2.7.1 @@ -19,6 +18,7 @@ require ( github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/acomagu/bufpipe v1.0.4 // indirect github.com/cloudflare/circl v1.3.2 // indirect + github.com/emersion/go-mbox v1.0.3 // indirect github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/go.sum b/go.sum index e53c191..fdb9a9a 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-mbox v1.0.3 h1:Kac75r/EGi6KZAz48HXal9q7EiaXNl+U5HZfyDz0LKM= +github.com/emersion/go-mbox v1.0.3/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/main.go b/main.go deleted file mode 100644 index bbd3f85..0000000 --- a/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package email - -import ( - "fmt" - "net/mail" - "time" - - "github.com/emersion/go-smtp" - - "git.derelict.garden/bryon/vault" - "git.derelict.garden/dovel/email/model" -) - -type handler func(mail mail.Message) error - -var handlers = map[string]handler{} - -func Register(domain string, h handler) error { - if s == nil { - return fmt.Errorf("dovel is not running") - } - handlers[domain] = h - return nil -} - -func Deregister(domain string) { - delete(handlers, domain) -} - -func GetHandler(domain string) handler { - return handlers[domain] -} - -var ( - s *smtp.Server - v vault.Vault[model.WebUser] -) - -func Start(cfg model.Config) error { - // TODO: make optional - var err error - v, err = vault.NewJSONPlainTextVault[model.WebUser](cfg.UsersFile) - if err != nil { - return err - } - s = smtp.NewServer(backend{}) - s.Addr = cfg.Server.Address - s.Domain = cfg.Server.Domain - s.ReadTimeout = 10 * time.Second - s.WriteTimeout = 10 * time.Second - s.MaxMessageBytes = 1024 * 1024 - s.MaxRecipients = 2 - s.AllowInsecureAuth = true - - println("starting mail server at", s.Addr) - return s.ListenAndServe() -}
commit 95eb49edf4ffe13032575c73b69917a26b1caba7 Author: blmayer <bleemayer@gmail.com> Date: Wed Sep 6 19:28:42 2023 -0300 Improved handler model diff --git a/model/main.go b/model/main.go index f7e5784..7a479d7 100644 --- a/model/main.go +++ b/model/main.go @@ -16,54 +16,15 @@ import ( "github.com/OfimaticSRL/parsemail" ) -// Config is the configuration structure used to set up dovel -// server and web interface. This should be a JSON file located -// in $HOME/.dovel-config.json -// Server field configures the server, and WebPort is an optional field -// used to specify the HTTP server port for the web interface, if present -// the HTTP web server is started. -type Config struct { - Server ServerConfig - Web WebConfig - WebPort *string - UsersFile string -} - -// ServerConfig is used to configure your email server. -// Address is used to specify which address the server should listen -// to connections, a typicall value is :2525. +// 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. // Domain is what the server should respond in a HELO or EHLO request. -// Inboxes are the accounts available is this server, i.e. the accepted -// domains on emails: "example@example.domain". Each domain is handled -// separetly, so you can receive email for multiple domains. -// Lastly DKIM fields should be the path to the keys, they are optional. -type ServerConfig struct { - Address string - Domain string - Inboxes []InboxConfig -} - -// WebConfig is used to configure your static server. -// Port is used to specify which address the server should listen -// to connections, a typical value is :8080. -// Lastly, Root specifies where your html files are. -type WebConfig struct { - Port string - Root string -} - -type InboxConfig struct { - Handler string - HTMLConfig -} - -type HTMLConfig struct { - CommonConfig - Out string - IndexTpl string - ListTpl string - MailsTpl string - MailTpl string +// Handlers is a list of domains that are allowed to send/receive emails. +type Config struct { + Port string + Domain string + VaultFile string } type tz struct { @@ -71,12 +32,6 @@ type tz struct { Offset int } -type CommonConfig struct { - Domain string - PrivateKey string - Root string -} - type WebUser struct { Name string Email string