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

cc638a4

Author: blmayer (bleemayer@gmail.com)

Date: Sat Aug 26 18:21:03 2023 -0300

Parent: 63cc35b

Changed to shared library model

Diff

backend.go

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

cmd/dovel/main.go

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

doc.go

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

main.go

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

model/main.go

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.