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