This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Sat Aug 26 18:21:03 2023 -0300
Parent: 63cc35b
Changed to shared library model
commit cc638a426af3c44c915b2832edf27c90e44c737a Author: blmayer <bleemayer@gmail.com> Date: Sat Aug 26 18:21:03 2023 -0300 Changed to shared library model diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..a455fbf --- /dev/null +++ b/backend.go @@ -0,0 +1,179 @@ +package email + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "net/mail" + "net/textproto" + "strings" + + "git.derelict.garden/dovel/email/util/wkd" + "github.com/ProtonMail/gopenpgp/v2/helper" + "github.com/emersion/go-msgauth/dkim" + "github.com/emersion/go-smtp" +) + +// A Session is returned after EHLO. +type Session struct { + domain string + user string + from string + tos []string +} + +func (s *Session) AuthPlain(username, password string) error { + println("connection sent", username, password) + if v.Validate(username, password) { + 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(r io.Reader) error { + content, _ := ioutil.ReadAll(r) + + from, err := mail.ParseAddress(s.from) + if err != nil { + println("parse address", err.Error()) + return err + } + s.domain = strings.Split(from.Address, "@")[1] + toList := strings.Join(s.tos, ", ") + if _, ok := handlers[s.domain]; ok { + if s.user == "" { + return fmt.Errorf("needs auth") + } + err = s.Send(s.from, toList, bytes.NewReader(content)) + if err != nil { + println("send error", err.Error()) + return err + } + } + + tos, err := mail.ParseAddressList(toList) + if err != nil { + println("parse addresslist", err.Error()) + return err + } + msg, err := mail.ReadMessage(strings.NewReader(string(content))) + for _, to := range tos { + localdom := strings.Split(to.Address, "@") + h, ok := handlers[localdom[1]] + if !ok { + continue + } + if err := h(*msg); 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, to string, raw io.Reader) error { + email, err := mail.ReadMessage(raw) + if err != nil { + return err + } + addrs, err := mail.ParseAddressList(to) + if err != nil { + return err + } + tos := []string{} + for _, addr := range addrs { + tos = append(tos, addr.Address) + } + + body, err := ioutil.ReadAll(email.Body) + if err != nil { + return err + } + for _, to := range addrs { + msg := string(body) + + // dns mx for email + addr := strings.Split(to.Address, "@") + 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: s.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 cc638a426af3c44c915b2832edf27c90e44c737a Author: blmayer <bleemayer@gmail.com> Date: Sat Aug 26 18:21:03 2023 -0300 Changed to shared library model diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go index a4effde..828d69e 100644 --- a/cmd/dovel/main.go +++ b/cmd/dovel/main.go @@ -2,27 +2,11 @@ package main import ( "encoding/json" - "log" - "net/http" "os" "path" - "time" + "git.derelict.garden/dovel/email" "git.derelict.garden/dovel/email/model" - "git.derelict.garden/dovel/email/model/file" - "git.derelict.garden/dovel/email/model/gwi" - "git.derelict.garden/dovel/email/model/html" - "git.derelict.garden/dovel/email/model/raw" - - "blmayer.dev/x/vault" - - "github.com/emersion/go-smtp" -) - -var ( - defaultPort = "8080" - cfg model.Config - b = backend{Handlers: map[string]model.Mailer{}} ) func main() { @@ -33,78 +17,13 @@ func main() { } configFile, err := os.Open(path.Join(configPath, "dovel", "config.json")) if err != nil { - println("open config", err.Error(), ". Using defaults") - return - } - json.NewDecoder(configFile).Decode(&cfg) - - for _, hand := range cfg.Server.Inboxes { - switch hand.Handler { - case "gwi": - // load gwi user file - v, err := vault.NewJSONPlainTextVault[model.WebUser](path.Join(hand.Root, "users.json")) - if err != nil { - panic(err) - } - g, err := gwi.NewGWIHandler(hand.CommonConfig, v) - if err != nil { - panic(err) - } - - b.Handlers[hand.Domain] = g - case "file": - v, err := vault.NewJSONPlainTextVault[model.WebUser](path.Join(hand.Root, "users.json")) - if err != nil { - panic(err) - } - - mail, err := file.NewFileHandler(hand.CommonConfig, v) - if err != nil { - panic(err) - } - - b.Handlers[hand.Domain] = mail - case "raw": - mail, err := raw.NewRawHandler(hand.CommonConfig) - if err != nil { - panic(err) - } - - b.Handlers[hand.Domain] = mail - case "html": - hand.HTMLConfig.CommonConfig = hand.CommonConfig - mail, err := html.NewHTMLHandler(hand.HTMLConfig) - if err != nil { - panic(err) - } - - b.Handlers[hand.Domain] = mail - } - println("added handler " + hand.Domain + " as " + hand.Handler) + panic(err) } - s := smtp.NewServer(b) - - 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 - - if cfg.WebPort != nil { - println("starting web server at :" + *cfg.WebPort) - go func() { - err = http.ListenAndServe(":"+*cfg.WebPort, nil) - if err != nil { - panic(err) - } - }() - } + cfg := model.Config{} + json.NewDecoder(configFile).Decode(&cfg) - println("starting mail server at", s.Addr) - if err := s.ListenAndServe(); err != nil { - log.Fatal(err) + if err := email.Start(cfg); err != nil { + panic(err) } }
commit cc638a426af3c44c915b2832edf27c90e44c737a Author: blmayer <bleemayer@gmail.com> Date: Sat Aug 26 18:21:03 2023 -0300 Changed to shared library model diff --git a/doc.go b/doc.go index 91a9d7b..c71b848 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,4 @@ -// dovel is a SMTP server and web interface that is simple to setup and +// email is a SMTP server and web interface that is simple to setup and // use. // // This package has two parts: @@ -59,4 +59,4 @@ // sending email is more complicated, some receiving servers only need the SPF // and PTR records, some also need DKIM and DMARK, adjust according to your // needs. -package dovel +package email
commit cc638a426af3c44c915b2832edf27c90e44c737a Author: blmayer <bleemayer@gmail.com> Date: Sat Aug 26 18:21:03 2023 -0300 Changed to shared library model diff --git a/main.go b/main.go new file mode 100644 index 0000000..bbd3f85 --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +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 cc638a426af3c44c915b2832edf27c90e44c737a Author: blmayer <bleemayer@gmail.com> Date: Sat Aug 26 18:21:03 2023 -0300 Changed to shared library model diff --git a/model/main.go b/model/main.go index b27ff34..f7e5784 100644 --- a/model/main.go +++ b/model/main.go @@ -23,9 +23,10 @@ import ( // 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 + Server ServerConfig + Web WebConfig + WebPort *string + UsersFile string } // ServerConfig is used to configure your email server.