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.