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

e737530

Author: blmayer (bleemayer@gmail.com)

Date: Tue Apr 18 19:15:34 2023 -0300

Parent: 2abc125

Improved sending email

Diff

TODO.txt

commit e737530dfcc9a336d95519924f6a13f301720e10
Author: blmayer <bleemayer@gmail.com>
Date:   Tue Apr 18 19:15:34 2023 -0300

    Improved sending email

diff --git a/TODO.txt b/TODO.txt
index d714e35..4761707 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -7,6 +7,8 @@
 ☐ Support PGP
 ✓ Support DKIM if needed
 ☐ Improve web users
+☐ Add insert attachments button
+☐ Use XDG desktop for config path
 ☐ Support SMTP for sending email?
 ☐ Add config menu
 ☐ Add denylist filtering

cmd/dovel/main.go

commit e737530dfcc9a336d95519924f6a13f301720e10
Author: blmayer <bleemayer@gmail.com>
Date:   Tue Apr 18 19:15:34 2023 -0300

    Improved sending email

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index 105d18f..4d435d8 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -52,7 +52,7 @@ func main() {
 		switch hand.Handler {
 		case "gwi":
 			// load gwi user file
-			v, err := NewPGPVault(path.Join(hand.Root, "users.json"))
+			v, err := NewPlainTextVault(path.Join(hand.Root, "users.json"))
 			if err != nil {
 				panic(err)
 			}
@@ -63,15 +63,20 @@ func main() {
 
 			b.Handlers[hand.Domain] = g.Save
 		case "file":
+			v, err := NewPlainTextVault(path.Join(hand.Root, "users.json"))
+			if err != nil {
+				panic(err)
+			}
+
 			funcs := map[string]any{"heading": heading}
-			mail, err := file.NewFileHandler(hand, funcs)
+			mail, err := file.NewFileHandler(hand, v, funcs)
 			if err != nil {
 				panic(err)
 			}
 
 			if hand.Templates != "" {
 				http.HandleFunc(hand.Domain+"/", mail.IndexHandler())
-				http.HandleFunc(hand.Domain+"/assets/", newAssetsHandler(hand.Templates))
+				http.HandleFunc(hand.Domain+"/assets/", mail.AssetsHandler())
 				http.HandleFunc(hand.Domain+"/out", mail.SendHandler(b))
 			}
 
@@ -100,14 +105,8 @@ func main() {
 		}()
 	}
 
-	println("Starting mail server at", s.Addr)
+	println("starting mail server at", s.Addr)
 	if err := s.ListenAndServe(); err != nil {
 		log.Fatal(err)
 	}
 }
-
-func newAssetsHandler(root string) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, path.Join(root, r.URL.Path[1:]))
-	}
-}

cmd/dovel/vault.go

commit e737530dfcc9a336d95519924f6a13f301720e10
Author: blmayer <bleemayer@gmail.com>
Date:   Tue Apr 18 19:15:34 2023 -0300

    Improved sending email

diff --git a/cmd/dovel/vault.go b/cmd/dovel/vault.go
index 1cc6d12..d8b7a72 100644
--- a/cmd/dovel/vault.go
+++ b/cmd/dovel/vault.go
@@ -7,30 +7,30 @@ import (
 	"blmayer.dev/x/dovel/interfaces"
 )
 
-type pgpUser struct {
-	Name string
-	PGP string
-	Address string
+type plainTextUser struct {
+	Name     string
+	Address  string
+	Password string
 }
 
-func (u pgpUser) Email() string {
+func (u plainTextUser) Email() string {
 	return u.Address
 }
 
-func (u pgpUser) Login() string {
+func (u plainTextUser) Login() string {
 	return u.Name
 }
 
-func (u pgpUser) Pass() string {
-	return u.PGP
+func (u plainTextUser) Pass() string {
+	return u.Password
 }
 
-type pgpVault struct {
-	Users []pgpUser
+type plainTextVault struct {
+	Users []plainTextUser
 }
 
-func NewPGPVault(path string) (interfaces.Vault, error) {
-	s := pgpVault{Users: []pgpUser{}}
+func NewPlainTextVault(path string) (interfaces.Vault, error) {
+	s := plainTextVault{Users: []plainTextUser{}}
 
 	file, err := os.Open(path)
 	if err != nil {
@@ -46,7 +46,7 @@ func NewPGPVault(path string) (interfaces.Vault, error) {
 	return s, nil
 }
 
-func (f pgpVault) GetUser(login string) interfaces.User {
+func (f plainTextVault) GetUser(login string) interfaces.User {
 	for _, u := range f.Users {
 		if u.Name == login {
 			return u
@@ -56,6 +56,13 @@ func (f pgpVault) GetUser(login string) interfaces.User {
 }
 
 // Validate is not used here. Only here to fill the interface
-func (f pgpVault) Validate(login, pass string) bool {
-	return false
+func (f plainTextVault) Validate(login, pass string) bool {
+	user := f.GetUser(login)
+	if user == nil {
+		return false
+	}
+	if user.Pass() != pass {
+		return false
+	}
+	return true
 }

interfaces/backend/backend.go

commit e737530dfcc9a336d95519924f6a13f301720e10
Author: blmayer <bleemayer@gmail.com>
Date:   Tue Apr 18 19:15:34 2023 -0300

    Improved sending email

diff --git a/interfaces/backend/backend.go b/interfaces/backend/backend.go
index 6348a25..9454180 100644
--- a/interfaces/backend/backend.go
+++ b/interfaces/backend/backend.go
@@ -4,21 +4,14 @@
 package backend
 
 import (
-	"bytes"
 	"crypto"
 	"fmt"
 	"io"
-	"mime/multipart"
-	"net"
-	"net/http"
-	"strconv"
 	"strings"
-	"time"
 
 	"blmayer.dev/x/dovel/config"
 	"blmayer.dev/x/dovel/interfaces"
 	"github.com/OfimaticSRL/parsemail"
-	"github.com/emersion/go-msgauth/dkim"
 	"github.com/emersion/go-smtp"
 )
 
@@ -92,127 +85,3 @@ func (b Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
 	return Session{handlers: b.Handlers}, nil
 }
 
-func (b Backend) SendHandler(saveFunction config.Handler) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		println("backend sending email")
-		if err := r.ParseMultipartForm(20 * 1024 * 1024); err != nil {
-			println("form error: " + err.Error())
-			http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable)
-			return
-		}
-		fo := r.MultipartForm
-
-		body := bytes.Buffer{}
-		form := multipart.NewWriter(&body)
-		email := interfaces.Email{
-			From:    fo.Value["from"][0],
-			To:      []string{},
-			Cc:      []string{},
-			Subject: fo.Value["subject"][0],
-			Date:    time.Now(),
-		}
-		email.ID = fmt.Sprintf("%s%s", strconv.FormatInt(email.Date.Unix(), 10), email.From)
-
-		// headers
-		body.WriteString("MIME-Version: 1.0\r\n")
-		body.WriteString("From: " + email.From + "\r\n")
-
-		for _, to := range fo.Value["to"] {
-			email.To = append(email.To, fmt.Sprintf("%s", to))
-		}
-		body.WriteString("To: " + strings.Join(email.To, ", ") + "\r\n")
-
-		if len(fo.Value["cc"]) > 0 && fo.Value["cc"][0] != "" {
-			for _, cc := range fo.Value["cc"] {
-				email.Cc = append(email.Cc, fmt.Sprintf("%s", cc))
-			}
-			body.WriteString("Cc: " + strings.Join(email.Cc, ", ") + "\r\n")
-		}
-
-		body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", email.ID))
-
-		body.WriteString("Date: " + email.Date.Format(time.RFC1123Z) + "\r\n")
-		body.WriteString("Subject: " + email.Subject + "\r\n")
-		body.WriteString("Content-Type: multipart/mixed; boundary=" + form.Boundary() + "\r\n\r\n")
-
-		text, err := form.CreatePart(
-			map[string][]string{
-				"Content-Type": {"text/plain; charset=\"UTF-8\""},
-			},
-		)
-		if err != nil {
-			println("creatPart error: " + err.Error())
-			http.Error(w, "creatPart error"+err.Error(), http.StatusInternalServerError)
-			return
-		}
-		text.Write([]byte(strings.Join(fo.Value["body"], "")))
-
-		for name, fi := range fo.File {
-			part, err := form.CreateFormFile(name, name)
-			if err != nil {
-				println("error creating form file: " + err.Error())
-				continue
-			}
-			attach, err := fi[0].Open()
-			if err != nil {
-				println("error getting attachment: " + err.Error())
-				continue
-			}
-			defer attach.Close()
-
-			content, err := io.ReadAll(attach)
-			if err != nil {
-				println("error getting attachment: " + err.Error())
-				continue
-			}
-			part.Write(content)
-		}
-		form.Close()
-
-		// dkim
-		payload := bytes.Buffer{}
-		options := &dkim.SignOptions{
-			Domain:   "mail.blmayer.dev",
-			Selector: "dkim",
-			Signer:   b.PrivateKey,
-		}
-		if err := dkim.Sign(&payload, &body, options); err != nil {
-			println("failed to sign body:", err.Error())
-		}
-		email.Body = payload.String()
-		email.Raw = []byte(email.Body)
-
-		// dns mx for email
-		for _, to := range email.To {
-			addr := strings.Split(to, "@")
-			mxs, err := net.LookupMX(addr[1])
-			if err != nil {
-				println("get MX error: " + err.Error())
-				http.Error(w, "get MX error"+err.Error(), http.StatusInternalServerError)
-				return
-			}
-			if len(mxs) == 0 {
-				println("empty MX response")
-				http.Error(w, "empty MX response", http.StatusInternalServerError)
-				return
-			}
-
-			server := mxs[0].Host + ":smtp"
-			err = smtp.SendMail(
-				server,
-				nil,
-				email.From,
-				[]string{to},
-				bytes.NewReader(email.Raw),
-			)
-			if err != nil {
-				println("sendMail error: " + err.Error())
-				http.Error(w, "error sending email: "+err.Error(), http.StatusInternalServerError)
-				return
-			}
-
-			saveFunction(email)
-		}
-		http.Redirect(w, r, "/", http.StatusFound)
-	}
-}

interfaces/file/file.go

commit e737530dfcc9a336d95519924f6a13f301720e10
Author: blmayer <bleemayer@gmail.com>
Date:   Tue Apr 18 19:15:34 2023 -0300

    Improved sending email

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
index d76463e..d1201c3 100644
--- a/interfaces/file/file.go
+++ b/interfaces/file/file.go
@@ -41,13 +41,14 @@ import (
 type FileHandler struct {
 	templates  *template.Template
 	root       string
-	password   string
+	assetsPath string
 	domain     string
 	privateKey crypto.Signer
+	vault      interfaces.Vault
 }
 
-func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error) {
-	f := FileHandler{root: c.Root, password: c.Password, domain: c.Domain}
+func NewFileHandler(c config.InboxConfig, v interfaces.Vault, fs map[string]any) (FileHandler, error) {
+	f := FileHandler{root: c.Root, vault: v, domain: c.Domain}
 	if fs == nil {
 		fs = map[string]any{}
 	}
@@ -57,6 +58,7 @@ func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error
 
 	var err error
 	if c.Templates != "" {
+		f.assetsPath = c.Templates
 		f.templates, err = template.New(c.Root).Funcs(fs).
 			ParseGlob(c.Templates + "/*.html")
 	}
@@ -84,7 +86,7 @@ func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error
 func (f FileHandler) IndexHandler() http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		user, pass, _ := r.BasicAuth()
-		if user != "x" || pass != f.password {
+		if user == "" || !f.vault.Validate(user, pass) {
 			w.Header().Add("WWW-Authenticate", "Basic")
 			http.Error(w, "wrong auth", http.StatusUnauthorized)
 			return
@@ -104,17 +106,66 @@ func (f FileHandler) IndexHandler() http.HandlerFunc {
 	}
 }
 
+func (f FileHandler) AssetsHandler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, path.Join(f.assetsPath, r.URL.Path[1:]))
+	}
+}
+
 func (f FileHandler) SendHandler(b backend.Backend) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		println("file handling send email")
 		user, pass, _ := r.BasicAuth()
-		if user != "x" || pass != f.password {
+		if user == "" || !f.vault.Validate(user, pass) {
 			w.Header().Add("WWW-Authenticate", "Basic")
 			http.Error(w, "wrong auth", http.StatusUnauthorized)
 			return
 		}
 
-		b.SendHandler(f.SaveSent)(w, r)
+		if err := r.ParseMultipartForm(20 * 1024 * 1024); err != nil {
+			println("form error: " + err.Error())
+			http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable)
+			return
+		}
+		fo := r.MultipartForm
+
+		email := interfaces.Email{
+			From:        fo.Value["from"][0],
+			To:          fo.Value["to"],
+			Cc:          fo.Value["cc"],
+			Subject:     fo.Value["subject"][0],
+			Date:        time.Now(),
+			Body:        fo.Value["body"][0],
+			Attachments: map[string]interfaces.Attachment{},
+		}
+
+		for name, fi := range fo.File {
+			attach, err := fi[0].Open()
+			if err != nil {
+				println("error getting attachment: " + err.Error())
+				continue
+			}
+			defer attach.Close()
+
+			content, err := io.ReadAll(attach)
+			if err != nil {
+				println("error getting attachment: " + err.Error())
+				continue
+			}
+			email.Attachments[name] = interfaces.Attachment{
+				Name: name,
+				Data: content,
+				// TODO: Add content-type
+			}
+		}
+		
+		err := f.Send(email)
+		if err != nil {
+			println("send error: " + err.Error())
+			http.Error(w, "send error"+err.Error(), http.StatusInternalServerError)
+			return
+		}
+		http.Redirect(w, r, "/", http.StatusFound)
 	}
 }
 
@@ -209,7 +260,7 @@ func (f FileHandler) Send(mail interfaces.Email) error {
 	if err := dkim.Sign(&payload, &body, options); err != nil {
 		println("failed to sign body:", err.Error())
 	}
-	mail.Body = payload.String()
+	mail.Raw = payload.Bytes()
 
 	// dns mx for email
 	for _, to := range mail.To {
@@ -228,7 +279,7 @@ func (f FileHandler) Send(mail interfaces.Email) error {
 			nil,
 			mail.From,
 			[]string{to},
-			[]byte(mail.Body),
+			mail.Raw,
 		)
 		if err != nil {
 			return err