list

server

This is the main dovel repository, it has the Go code to run dovel SMTP server.

curl https://dovel.email/server.tar tar

95eb49e

Author: blmayer (bleemayer@gmail.com)

Date: Wed Sep 6 19:28:42 2023 -0300

Parent: cc638a4

Improved handler model

Diff

cmd/dovel/backend.go

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
+}

cmd/dovel/main.go

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)
 	}
 }

go.mod

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

go.sum

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=

main.go

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()
-}

model/main.go

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