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

2abc125

Author: blmayer (bleemayer@gmail.com)

Date: Mon Apr 17 20:57:54 2023 -0300

Parent: e0125f9

Made progress on user auth

Diff

cmd/dovel/main.go

commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date:   Mon Apr 17 20:57:54 2023 -0300

    Made progress on user auth

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index b7d2cb4..105d18f 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -21,15 +21,15 @@ var (
 	defaultConfig = config.Config{
 		WebPort: &defaultPort,
 		Server: config.ServerConfig{
-			Domain:      "dovel.email",
-			Address:     ":2525",
+			Domain:  "dovel.email",
+			Address: ":2525",
 			Inboxes: []config.InboxConfig{
 				{
-					Domain:    "localhost",
+					Domain:      "localhost",
 					DKIMKeyPath: "dkim.priv",
-					Templates: "www",
-					Handler:   "file",
-					Root:      "mail",
+					Templates:   "www",
+					Handler:     "file",
+					Root:        "mail",
 				},
 			},
 		},
@@ -51,12 +51,17 @@ func main() {
 	for _, hand := range cfg.Server.Inboxes {
 		switch hand.Handler {
 		case "gwi":
-			g, err := gwi.NewGWIHandler(hand)
+			// load gwi user file
+			v, err := NewPGPVault(path.Join(hand.Root, "users.json"))
+			if err != nil {
+				panic(err)
+			}
+			g, err := gwi.NewGWIHandler(hand, v)
 			if err != nil {
 				panic(err)
 			}
 
-			b.Handlers[hand.Domain] = g.GwiEmailHandler
+			b.Handlers[hand.Domain] = g.Save
 		case "file":
 			funcs := map[string]any{"heading": heading}
 			mail, err := file.NewFileHandler(hand, funcs)
@@ -106,5 +111,3 @@ func newAssetsHandler(root string) http.HandlerFunc {
 		http.ServeFile(w, r, path.Join(root, r.URL.Path[1:]))
 	}
 }
-
-

cmd/dovel/vault.go

commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date:   Mon Apr 17 20:57:54 2023 -0300

    Made progress on user auth

diff --git a/cmd/dovel/vault.go b/cmd/dovel/vault.go
new file mode 100644
index 0000000..1cc6d12
--- /dev/null
+++ b/cmd/dovel/vault.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"encoding/json"
+	"os"
+
+	"blmayer.dev/x/dovel/interfaces"
+)
+
+type pgpUser struct {
+	Name string
+	PGP string
+	Address string
+}
+
+func (u pgpUser) Email() string {
+	return u.Address
+}
+
+func (u pgpUser) Login() string {
+	return u.Name
+}
+
+func (u pgpUser) Pass() string {
+	return u.PGP
+}
+
+type pgpVault struct {
+	Users []pgpUser
+}
+
+func NewPGPVault(path string) (interfaces.Vault, error) {
+	s := pgpVault{Users: []pgpUser{}}
+
+	file, err := os.Open(path)
+	if err != nil {
+		return s, err
+	}
+	defer file.Close()
+
+	err = json.NewDecoder(file).Decode(&s.Users)
+	if err != nil {
+		return s, err
+	}
+
+	return s, nil
+}
+
+func (f pgpVault) GetUser(login string) interfaces.User {
+	for _, u := range f.Users {
+		if u.Name == login {
+			return u
+		}
+	}
+	return nil
+}
+
+// Validate is not used here. Only here to fill the interface
+func (f pgpVault) Validate(login, pass string) bool {
+	return false
+}

interfaces/gwi/gwi.go

commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date:   Mon Apr 17 20:57:54 2023 -0300

    Made progress on user auth

diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
index 30dd74e..d681671 100644
--- a/interfaces/gwi/gwi.go
+++ b/interfaces/gwi/gwi.go
@@ -2,13 +2,24 @@
 package gwi
 
 import (
+	"bytes"
+	"crypto"
+	"crypto/x509"
+	"encoding/pem"
 	"fmt"
+	"io/ioutil"
+	"mime/multipart"
+	"net"
+	"net/smtp"
 	"os"
 	"path"
+	"strconv"
 	"strings"
+	"time"
 
 	"blmayer.dev/x/dovel/config"
 	"blmayer.dev/x/dovel/interfaces"
+	"github.com/emersion/go-msgauth/dkim"
 )
 
 // GWIConfig is used to configure the GWI interface for dovel. Root is the
@@ -17,17 +28,37 @@ import (
 // key of the map specifies a trigger in the form "key!". That is, if the email
 // body starts with key! then the command key is run.
 type GWIHandler struct {
-	Root     string
-	Commands map[string]func(email interfaces.Email) error
+	Root       string
+	Commands   map[string]func(email interfaces.Email) error
+	domain     string
+	privateKey crypto.Signer
+	vault      interfaces.Vault
 }
 
-func NewGWIHandler(c config.InboxConfig) (GWIHandler, error) {
-	return GWIHandler{Root: c.Root}, nil
+func NewGWIHandler(c config.InboxConfig, vault interfaces.Vault) (GWIHandler, error) {
+	g := GWIHandler{Root: c.Root, domain: c.Domain, vault: vault}
+	if c.DKIMKeyPath != "" {
+		key, err := ioutil.ReadFile(c.DKIMKeyPath)
+		if err != nil {
+			return g, err
+		}
+
+		block, _ := pem.Decode(key)
+		if block == nil {
+			return g, err
+		}
+		privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err != nil {
+			return g, err
+		}
+		g.privateKey = privKey.(crypto.Signer)
+	}
+	return g, nil
 }
 
-// GwiEmailHandler saves emails to the correct repo using the subject to
-// separate them. Subject fiel must be of form "[%s] %s".
-func (g GWIHandler) GwiEmailHandler(email interfaces.Email) error {
+// Save saves emails to the correct repo using the subject to
+// separate them. Subject field must be of form "[%s] %s".
+func (g GWIHandler) Save(email interfaces.Email) error {
 	userRepoDomain := strings.Split(email.To[0], "@")
 	userRepo := strings.Split(userRepoDomain[0], "/")
 	if len(userRepo) != 2 {
@@ -66,19 +97,120 @@ func (g GWIHandler) GwiEmailHandler(email interfaces.Email) error {
 	}
 
 	// apply commands
-	go func() {
-		println("gwi applying commands")
-		for com, f := range g.Commands {
-			println("gwi applying", com)
-			if !strings.HasPrefix(email.Body, "!"+com) {
-				continue
-			}
-			if err := f(email); err != nil {
-				println(com, "error", err.Error())
-				continue
-			}
-			println("gwi", com, "applied")
+	//go func() {
+	//	println("gwi applying commands")
+	//	for com, f := range g.Commands {
+	//		println("gwi applying", com)
+	//		if !strings.HasPrefix(email.Body, "!"+com) {
+	//			continue
+	//		}
+	//		if err := f(email); err != nil {
+	//			println(com, "error", err.Error())
+	//			continue
+	//		}
+	//		println("gwi", com, "applied")
+	//	}
+	//}()
+
+	// notify owner
+	owner := g.vault.GetUser(user)
+	if owner == nil || owner.Email() == "" {
+		return nil
+	}
+	email.To = []string{owner.Email()}
+	email.Body = fmt.Sprintf(
+		`You received an email with the subject %s.
+		
+		Check you project by visiting https://%s/%s/%s
+
+		Yours.
+
+		The GWI team.`,
+		email.Subject,
+		g.domain,
+		user,
+		repo,
+	)
+	email.Subject = "New mail on project " + repo
+	email.From = fmt.Sprintf("%s/%s@%s", user, repo, g.domain)
+	return g.Send(email)
+}
+
+func (g GWIHandler) Send(mail interfaces.Email) error {
+	mail.ID = fmt.Sprintf("%s%s", strconv.FormatInt(mail.Date.Unix(), 10), mail.From)
+
+	body := bytes.Buffer{}
+	form := multipart.NewWriter(&body)
+
+	// headers
+	body.WriteString("MIME-Version: 1.0\r\n")
+	body.WriteString("From: " + mail.From + "\r\n")
+	body.WriteString("To: " + strings.Join(mail.To, ", ") + "\r\n")
+	if len(mail.Cc) > 0 {
+		body.WriteString("Cc: " + strings.Join(mail.Cc, ", ") + "\r\n")
+	}
+
+	body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", mail.ID))
+
+	body.WriteString("Date: " + mail.Date.Format(time.RFC1123Z) + "\r\n")
+	body.WriteString("Subject: " + mail.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 {
+		return err
+	}
+	text.Write([]byte(mail.Body))
+
+	for name, fi := range mail.Attachments {
+		part, err := form.CreateFormFile(name, name)
+		if err != nil {
+			println("error creating form file: " + err.Error())
+			continue
+		}
+
+		part.Write(fi.Data)
+	}
+	form.Close()
+
+	// dkim
+	payload := bytes.Buffer{}
+	options := &dkim.SignOptions{
+		Domain:   g.domain,
+		Selector: "dkim",
+		Signer:   g.privateKey,
+	}
+	if err := dkim.Sign(&payload, &body, options); err != nil {
+		println("failed to sign body:", err.Error())
+	}
+	mail.Body = payload.String()
+
+	// dns mx for email
+	for _, to := range mail.To {
+		addr := strings.Split(to, "@")
+		mxs, err := net.LookupMX(addr[1])
+		if err != nil {
+			return err
 		}
-	}()
-	return err
+		if len(mxs) == 0 {
+			return err
+		}
+
+		server := mxs[0].Host + ":smtp"
+		err = smtp.SendMail(
+			server,
+			nil,
+			mail.From,
+			[]string{to},
+			[]byte(mail.Body),
+		)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }

interfaces/main.go

commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date:   Mon Apr 17 20:57:54 2023 -0300

    Made progress on user auth

diff --git a/interfaces/main.go b/interfaces/main.go
index eb5ccf8..09300a2 100644
--- a/interfaces/main.go
+++ b/interfaces/main.go
@@ -18,6 +18,11 @@ type User interface {
 	Pass() string
 }
 
+type Vault interface {
+	GetUser(login string) User
+	Validate(login, pass string) bool
+}
+
 type File struct {
 	*object.File
 	Size int64