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

c18a936

Author: b (git@mail.blmayer.dev)

Date: Thu Jul 20 20:35:59 2023 -0300

Parent: 1db678f

Added html handler

- Removed web part
- Renamed interfaces to model
- Moved sending email to main
- Improved configs

Diff

Makefile

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/Makefile b/Makefile
index b9f0f8c..4038943 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 .PHONY: clean deploy deploy-site deploy-assets deploy-pages
 
-dovel: $(wildcard cmd/dovel/*.go interfaces/*/*.go config/*.go interfaces/*.go)
+dovel: $(wildcard cmd/dovel/*.go model/*/*.go model/*.go util/*/*.go)
 	GOARCH=arm GOARM=6 go build -o $@ cmd/dovel/*.go
 
 deploy: dovel

cmd/dovel/backend.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/cmd/dovel/backend.go b/cmd/dovel/backend.go
index 998937a..421a6f2 100644
--- a/cmd/dovel/backend.go
+++ b/cmd/dovel/backend.go
@@ -1,27 +1,33 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
-	"crypto"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"net"
 	"net/mail"
+	"net/textproto"
 	"strings"
 
 	"blmayer.dev/x/vault"
 
-	"git.derelict.garden/dovel/email/interfaces"
+	"git.derelict.garden/dovel/email/model"
+	"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
-	handlers map[string]interfaces.Mailer
-	from string
-	tos   []string
-	vault vault.Vault[interfaces.WebUser]
+	handlers map[string]model.Mailer
+	from     string
+	tos      []string
+	vault    vault.Vault[model.WebUser]
 }
 
 func (s *Session) AuthPlain(username, password string) error {
@@ -52,49 +58,129 @@ func (s *Session) Data(r io.Reader) error {
 		println("parse address", err.Error())
 		return err
 	}
-	fromDomain := strings.Split(from.Address, "@")[1]
-	tos, err := mail.ParseAddressList(strings.Join(s.tos, ", "))
+	s.domain = strings.Split(from.Address, "@")[1]
+	toList := strings.Join(s.tos, ", ")
+	if _, ok := s.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
 	}
-
 	for _, to := range tos {
 		localdom := strings.Split(to.Address, "@")
+
 		if h, ok := s.handlers[localdom[1]]; ok {
 			err = h.Save(s.from, to.Address, bytes.NewReader(content))
-		} else {
-			if s.user == "" {
-				return fmt.Errorf("needs auth")
-			}
-			h, ok = s.handlers[fromDomain]
-			if !ok {
-				return fmt.Errorf("from is wrong")
+			if err != nil {
+				println("handler error", err.Error())
+				return err
 			}
-			err = h.Send(s.from, to.Address, bytes.NewReader(content))
-		}
-		if err != nil {
-			println("handler error", err.Error())
-			return err
 		}
 	}
 
 	return nil
 }
 
-func (s *Session) Reset() { }
+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:   s.vault.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 {
-	Handlers   map[string]interfaces.Mailer
-	PrivateKey crypto.Signer
+	Handlers   map[string]model.Mailer
 }
 
 func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
-	return &Session{handlers: b.Handlers, tos:[]string{}}, nil
+	return &Session{handlers: b.Handlers, tos: []string{}}, nil
 }
-

cmd/dovel/main.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index d046520..a4effde 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -8,11 +8,11 @@ import (
 	"path"
 	"time"
 
-	"git.derelict.garden/dovel/email/config"
-	"git.derelict.garden/dovel/email/interfaces"
-	"git.derelict.garden/dovel/email/interfaces/file"
-	"git.derelict.garden/dovel/email/interfaces/gwi"
-	"git.derelict.garden/dovel/email/interfaces/raw"
+	"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"
 
@@ -20,25 +20,9 @@ import (
 )
 
 var (
-	defaultPort   = "8080"
-	defaultConfig = config.Config{
-		WebPort: &defaultPort,
-		Server: config.ServerConfig{
-			Domain:  "dovel.email",
-			Address: ":2525",
-			Inboxes: []config.InboxConfig{
-				{
-					Domain:      "localhost",
-					DKIMKeyPath: "dkim.priv",
-					Templates:   "www",
-					Handler:     "file",
-					Root:        "mail",
-				},
-			},
-		},
-	}
-	cfg config.Config
-	b   = backend{Handlers: map[string]interfaces.Mailer{}}
+	defaultPort = "8080"
+	cfg         model.Config
+	b           = backend{Handlers: map[string]model.Mailer{}}
 )
 
 func main() {
@@ -50,39 +34,46 @@ func main() {
 	configFile, err := os.Open(path.Join(configPath, "dovel", "config.json"))
 	if err != nil {
 		println("open config", err.Error(), ". Using defaults")
-		cfg = defaultConfig
-	} else {
-		json.NewDecoder(configFile).Decode(&cfg)
+		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[interfaces.WebUser](path.Join(hand.Root, "users.json"))
+			v, err := vault.NewJSONPlainTextVault[model.WebUser](path.Join(hand.Root, "users.json"))
 			if err != nil {
 				panic(err)
 			}
-			g, err := gwi.NewGWIHandler(hand, v)
+			g, err := gwi.NewGWIHandler(hand.CommonConfig, v)
 			if err != nil {
 				panic(err)
 			}
 
 			b.Handlers[hand.Domain] = g
 		case "file":
-			v, err := vault.NewJSONPlainTextVault[interfaces.WebUser](path.Join(hand.Root, "users.json"))
+			v, err := vault.NewJSONPlainTextVault[model.WebUser](path.Join(hand.Root, "users.json"))
 			if err != nil {
 				panic(err)
 			}
 
-			mail, err := file.NewFileHandler(hand, v, nil)
+			mail, err := file.NewFileHandler(hand.CommonConfig, v)
 			if err != nil {
 				panic(err)
 			}
 
 			b.Handlers[hand.Domain] = mail
 		case "raw":
-			mail, err := raw.NewRawHandler(hand)
+			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)
 			}

config/config.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/config/config.go b/config/config.go
deleted file mode 100644
index 667a948..0000000
--- a/config/config.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package config
-
-// 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
-	WebPort *string
-}
-
-type InboxConfig struct {
-	Domain      string
-	DKIMKeyPath string
-	PrivateKey  string
-	Templates   string
-	Handler     string
-	Root        string
-	Password    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.
-// 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
-}

dovel

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/dovel b/dovel
deleted file mode 100755
index c4466aa..0000000
Binary files a/dovel and /dev/null differ

interfaces/file/file.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
deleted file mode 100644
index 5db9a65..0000000
--- a/interfaces/file/file.go
+++ /dev/null
@@ -1,314 +0,0 @@
-// file adds the common email file handling, but with a twist: here we save
-// emails first by inbox and then subject. This package also delivers some
-// functions to be used in the templates, for the web interface.
-package file
-
-import (
-	"bufio"
-	"bytes"
-	"crypto"
-	"crypto/x509"
-	"encoding/pem"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net"
-	"net/mail"
-	"net/smtp"
-	"net/textproto"
-	"os"
-	"path"
-	"sort"
-	"strings"
-
-	"git.derelict.garden/dovel/email/config"
-	"git.derelict.garden/dovel/email/interfaces"
-	"git.derelict.garden/dovel/email/util/wkd"
-
-	"blmayer.dev/x/vault"
-
-	"github.com/OfimaticSRL/parsemail"
-	"github.com/emersion/go-msgauth/dkim"
-
-	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
-	"github.com/ProtonMail/gopenpgp/v2/helper"
-)
-
-// FileHandler is used to configure the file email handler.
-// Root is where mail should be saved, Templates is an optional field that
-// is the path to templates, to be used in the web server; Password is the
-// password for the x user, for now this is very simple; Domain is used to
-// filter and separate emails, only emails sent to one of your domains are
-// saved, each according to its configuration.
-type FileHandler struct {
-	root       string
-	domain     string
-	privateKey crypto.Signer
-	keyText string
-	vault      vault.Vault[interfaces.WebUser]
-}
-
-func NewFileHandler(c config.InboxConfig, v vault.Vault[interfaces.WebUser], fs map[string]any) (FileHandler, error) {
-	f := FileHandler{root: c.Root, vault: v, domain: c.Domain}
-	if fs == nil {
-		fs = map[string]any{}
-	}
-	fs["inboxes"] = f.Mailboxes
-	fs["mails"] = f.Mails
-	fs["mail"] = f.Mail
-
-	var err error
-	if c.DKIMKeyPath != "" {
-		key, err := ioutil.ReadFile(c.DKIMKeyPath)
-		if err != nil {
-			return f, err
-		}
-		f.keyText = string(key)
-
-		block, _ := pem.Decode(key)
-		if block == nil {
-			return f, err
-		}
-		privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
-		if err != nil {
-			return f, err
-		}
-		f.privateKey = privKey.(crypto.Signer)
-	}
-
-	return f, err
-}
-
-func (f FileHandler) Save(from, to string, r io.Reader) error {
-	m, err := parsemail.Parse(r)
-	if err != nil {
-		println("mail parse", err.Error())
-		return err
-	}
-
-	email := interfaces.ToEmail(m)
-	mailDir := path.Join(f.root, email.To[0], email.Subject)
-	os.MkdirAll(mailDir, os.ModeDir|0o700)
-
-	file, err := os.Create(path.Join(mailDir, email.ID))
-	if err != nil {
-		println("file create", err.Error())
-		return err
-	}
-	defer file.Close()
-
-	_, err = file.Write(email.Raw)
-	if err != nil {
-		println("file write", err.Error())
-		return err
-	}
-
-	return nil
-}
-
-func (f FileHandler) SaveSent(email interfaces.Email) error {
-	mailDir := path.Join(f.root, email.From, email.Subject)
-	os.MkdirAll(mailDir, os.ModeDir|0o700)
-
-	file, err := os.Create(path.Join(mailDir, email.ID))
-	if err != nil {
-		println("file create", err.Error())
-		return err
-	}
-	defer file.Close()
-
-	_, err = file.Write(email.Raw)
-	if err != nil {
-		println("file write", err.Error())
-		return err
-	}
-
-	return nil
-}
-
-func (h FileHandler) 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:   h.domain,
-			Selector: "dkim",
-			Signer:   h.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.Bytes(),
-		)
-		if err != nil {
-			return err
-		}
-
-	}
-	return h.Save(from, to, raw)
-}
-
-func (f FileHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {
-	fmt.Println("mailer mailboxes for", folder)
-	dir, err := os.ReadDir(path.Join(f.root, folder))
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var threads []interfaces.Mailbox
-	for _, d := range dir {
-		if !d.IsDir() {
-			continue
-		}
-
-		info, err := d.Info()
-		if err != nil {
-			fmt.Println("dir info", err.Error())
-			continue
-		}
-		t := interfaces.Mailbox{
-			Title:   d.Name(),
-			LastMod: info.ModTime(),
-		}
-
-		threads = append(threads, t)
-	}
-	sort.Slice(
-		threads,
-		func(i, j int) bool {
-			return threads[i].LastMod.After(threads[j].LastMod)
-		},
-	)
-
-	return threads, nil
-}
-
-func (f FileHandler) Mails(folder string) ([]interfaces.Email, error) {
-	fmt.Println("mailer mails for", folder)
-
-	dir := path.Join(f.root, folder)
-	threadDir, err := os.ReadDir(dir)
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var emails []interfaces.Email
-	for _, t := range threadDir {
-		mail, err := f.Mail(path.Join(folder, t.Name()))
-		if err != nil {
-			fmt.Println("mail error:", err.Error())
-			continue
-		}
-
-		emails = append(emails, mail)
-	}
-	sort.Slice(
-		emails,
-		func(i, j int) bool {
-			return emails[i].Date.Before(emails[j].Date)
-		},
-	)
-	fmt.Println("found", len(emails), "emails")
-
-	return emails, err
-}
-
-func (f FileHandler) Mail(file string) (interfaces.Email, error) {
-	fmt.Println("mailer getting", file)
-
-	mailFile, err := os.Open(path.Join(f.root, file))
-	if err != nil {
-		fmt.Println("open mail error:", err.Error())
-		return interfaces.Email{}, err
-	}
-	defer mailFile.Close()
-
-	mail, err := parsemail.Parse(mailFile)
-	if err != nil {
-		fmt.Println("email parse error:", err.Error())
-		return interfaces.Email{}, err
-	}
-
-	// try to decrypt
-	if f.privateKey != nil && pgp.IsPGPMessage(mail.TextBody) {
-		txt, err := helper.DecryptMessageArmored(f.keyText, nil, mail.TextBody)
-		if err != nil {
-			println(err)
-		}
-		mail.TextBody = txt
-	}
-	return interfaces.ToEmail(mail), nil
-}
-
-// Move is used to move messages between folders. The id parameter
-// is the message id to be moved and to is the destination folder.
-// Root is added automatically.
-func (f FileHandler) Move(id, to string) error {
-	return os.Rename(path.Join(f.root, id), path.Join(f.root, to, "/"))
-}
-
-// Delete is used to delete messages. The id parameter
-// is the message id to be deleted.
-// Root is added automatically.
-func (f FileHandler) Delete(id string) error {
-	return os.Remove(path.Join(f.root, id))
-}

interfaces/gwi/gwi.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
deleted file mode 100644
index f8356f5..0000000
--- a/interfaces/gwi/gwi.go
+++ /dev/null
@@ -1,365 +0,0 @@
-// gwi contains the email handler for interacting with gwi.
-package gwi
-
-import (
-	"bufio"
-	"bytes"
-	"crypto"
-	"crypto/x509"
-	"encoding/base64"
-	"encoding/pem"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net"
-	"net/mail"
-	"net/smtp"
-	"net/textproto"
-	"os"
-	"path"
-	"sort"
-	"strings"
-
-	"git.derelict.garden/dovel/email/config"
-	"git.derelict.garden/dovel/email/interfaces"
-	"git.derelict.garden/dovel/email/util/wkd"
-
-	"blmayer.dev/x/vault"
-
-	"github.com/OfimaticSRL/parsemail"
-	"github.com/ProtonMail/gopenpgp/v2/helper"
-	"github.com/emersion/go-msgauth/dkim"
-)
-
-// GWIConfig is used to configure the GWI interface for dovel. Root is the
-// same root as in GWI: the path to the repositories.
-// Commands lets you add functions that will run for each received email, the
-// 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
-	notify     bool
-	Commands   map[string]func(email interfaces.Email) error
-	domain     string
-	privateKey crypto.Signer
-	vault      vault.Vault[interfaces.WebUser]
-}
-
-func NewGWIHandler(c config.InboxConfig, vault vault.Vault[interfaces.WebUser]) (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
-}
-
-// 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(from, to string, r io.Reader) error {
-	m, err := parsemail.Parse(r)
-	if err != nil {
-		println("mail parse", err.Error())
-		return err
-	}
-
-	email := interfaces.ToEmail(m)
-	userRepoDomain := strings.Split(email.To[0], "@")
-	userRepo := strings.Split(userRepoDomain[0], "/")
-	if len(userRepo) != 2 {
-		println("received bad to", userRepo)
-		return fmt.Errorf("received bad to: %s", userRepo)
-	}
-
-	user, repo := userRepo[0], userRepo[1]
-	if _, err := os.Stat(path.Join(g.root, user, repo)); err != nil {
-		println("stat repo", err.Error())
-		return err
-	}
-
-	// split by subject
-	start := strings.Index(email.Subject, "[")
-	if start == -1 {
-		println("received bad subject", email.Subject)
-		return fmt.Errorf("Malformed subject field")
-	}
-	title := strings.TrimSpace(email.Subject[start:])
-
-	mailDir := path.Join(g.root, user, repo, "mail", title)
-	os.MkdirAll(mailDir, os.ModeDir|0o700)
-
-	mailFile, err := os.Create(path.Join(mailDir, email.ID))
-	if err != nil {
-		println("create mail file", err.Error())
-		return err
-	}
-	defer mailFile.Close()
-
-	_, err = mailFile.Write(email.Raw)
-	if err != nil {
-		println("write mail file", err.Error())
-		return err
-	}
-
-	// 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")
-	//	}
-	//}()
-
-	// notify owner
-	owner := g.vault.GetUser(user)
-	if owner.Name == "" {
-		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.From, strings.Join(email.To, ", "), strings.NewReader(email.Body))
-}
-
-func (h GWIHandler) 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:   h.domain,
-			Selector: "dkim",
-			Signer:   h.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.Bytes(),
-		)
-		if err != nil {
-			return err
-		}
-
-	}
-	return h.Save(from, to, raw)
-}
-
-func (f GWIHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {
-	fmt.Println("mailer mailboxes for", folder)
-	dir, err := os.ReadDir(path.Join(f.root, folder))
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var threads []interfaces.Mailbox
-	for _, d := range dir {
-		if !d.IsDir() {
-			continue
-		}
-
-		info, err := d.Info()
-		if err != nil {
-			fmt.Println("dir info", err.Error())
-			continue
-		}
-		t := interfaces.Mailbox{
-			Title:   d.Name(),
-			LastMod: info.ModTime(),
-		}
-
-		threads = append(threads, t)
-	}
-	sort.Slice(
-		threads,
-		func(i, j int) bool {
-			return threads[i].LastMod.After(threads[j].LastMod)
-		},
-	)
-
-	return threads, nil
-}
-
-func (f GWIHandler) Mails(folder string) ([]interfaces.Email, error) {
-	fmt.Println("mailer mails for", folder)
-
-	dir := path.Join(f.root, folder)
-	threadDir, err := os.ReadDir(dir)
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var emails []interfaces.Email
-	for _, t := range threadDir {
-		mail, err := f.Mail(path.Join(folder, t.Name()))
-		if err != nil {
-			fmt.Println("mail error:", err.Error())
-			continue
-		}
-
-		emails = append(emails, mail)
-	}
-	sort.Slice(
-		emails,
-		func(i, j int) bool {
-			return emails[i].Date.Before(emails[j].Date)
-		},
-	)
-	fmt.Println("found", len(emails), "emails")
-
-	return emails, err
-}
-
-func (g GWIHandler) Mail(file string) (interfaces.Email, error) {
-	fmt.Println("mailer getting", file)
-
-	mailFile, err := os.Open(path.Join(g.root, file))
-	if err != nil {
-		fmt.Println("open mail error:", err.Error())
-		return interfaces.Email{}, err
-	}
-	defer mailFile.Close()
-
-	mail, err := parsemail.Parse(mailFile)
-	if err != nil {
-		fmt.Println("email parse error:", err.Error())
-		return interfaces.Email{}, err
-	}
-
-	email := interfaces.Email{
-		ID:          mail.MessageID,
-		From:        mail.From[0].Address,
-		Date:        mail.Date,
-		Subject:     mail.Subject,
-		Body:        mail.TextBody,
-		Attachments: map[string]interfaces.Attachment{},
-	}
-	for _, to := range mail.To {
-		email.To = append(email.To, to.Address)
-	}
-
-	if len(mail.Cc) > 0 {
-		for _, cc := range mail.Cc {
-			email.Cc = append(email.Cc, cc.Address)
-		}
-	}
-
-	for _, a := range mail.Attachments {
-		content, err := io.ReadAll(a.Data)
-		if err != nil {
-			fmt.Println("read attachment", err.Error())
-			continue
-		}
-
-		encContent := base64.StdEncoding.EncodeToString(content)
-		email.Attachments[a.Filename] = interfaces.Attachment{
-			Name:        a.Filename,
-			ContentType: a.ContentType,
-			Data:        []byte(encContent),
-		}
-	}
-	return email, nil
-}
-
-// Move is used to move messages between folders. The id parameter
-// is the message id to be moved and to is the destination folder.
-// Root is added automatically.
-func (g GWIHandler) Move(id, to string) error {
-	return os.Rename(path.Join(g.root, id), path.Join(g.root, to, "/"))
-}
-
-// Delete is used to delete messages. The id parameter
-// is the message id to be deleted.
-// Root is added automatically.
-func (g GWIHandler) Delete(id string) error {
-	return os.Remove(path.Join(g.root, id))
-}

interfaces/raw/raw.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/interfaces/raw/raw.go b/interfaces/raw/raw.go
deleted file mode 100644
index 175a7e4..0000000
--- a/interfaces/raw/raw.go
+++ /dev/null
@@ -1,335 +0,0 @@
-// web adds the common email file handling, but with a twist: here we save
-// emails first by inbox and then subject. This package also delivers some
-// functions to be used in the templates, for the web interface.
-package raw
-
-import (
-	"bufio"
-	"bytes"
-	"crypto"
-	"crypto/x509"
-	"encoding/pem"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net"
-	"net/mail"
-	"net/smtp"
-	"net/textproto"
-	"os"
-	"path"
-	"sort"
-	"strings"
-	"text/template"
-	"time"
-
-	"git.derelict.garden/dovel/email/config"
-	"git.derelict.garden/dovel/email/interfaces"
-	"git.derelict.garden/dovel/email/util/wkd"
-
-	"github.com/OfimaticSRL/parsemail"
-	"github.com/emersion/go-msgauth/dkim"
-
-	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
-	"github.com/ProtonMail/gopenpgp/v2/helper"
-)
-
-// RawHandler is used to configure the file email handler.
-// Root is where mail should be saved, Templates is an optional field that
-// is the path to templates, to be used in the web server; Password is the
-// password for the x user, for now this is very simple; Domain is used to
-// filter and separate emails, only emails sent to one of your domains are
-// saved, each according to its configuration.
-type RawHandler struct {
-	root       string
-	domain     string
-	privateKey crypto.Signer
-	keyText string
-	templ *template.Template
-}
-
-func NewRawHandler(c config.InboxConfig) (RawHandler, error) {
-	h := RawHandler{root: c.Root, domain: c.Domain}
-
-	var err error
-	h.templ, err = template.ParseFiles(c.Templates)
-	if err != nil {
-		return h, err
-	}
-
-	if c.DKIMKeyPath != "" {
-		key, err := ioutil.ReadFile(c.DKIMKeyPath)
-		if err != nil {
-			return h, err
-		}
-		h.keyText = string(key)
-
-		block, _ := pem.Decode(key)
-		if block == nil {
-			return h, err
-		}
-		privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
-		if err != nil {
-			return h, err
-		}
-		h.privateKey = privKey.(crypto.Signer)
-	}
-
-	return h, err
-}
-
-func (h RawHandler) Save(from, to string, raw io.Reader) error {
-	mail, err := mail.ReadMessage(raw)
-	if err != nil {
-		return err
-	}
-
-	content, err := ioutil.ReadAll(mail.Body)
-	if err != nil {
-		println("file read", err.Error())
-		return err
-	}
-
-	// try to decrypt
-	if h.privateKey != nil && pgp.IsPGPMessage(string(content)) {
-		println("decrypting")
-		txt, err := helper.DecryptMessageArmored(
-			h.keyText, 
-			nil, 
-			string(content),
-		)
-		if err != nil {
-			println(err)
-		}
-		content = []byte(txt)
-	}
-
-	now := time.Now().Format(time.RFC3339)
-	subj := mail.Header.Get("Subject")
-	mailDir := path.Join(h.root, to, subj)
-	os.MkdirAll(mailDir, os.ModeDir|0o700)
-	name := fmt.Sprintf("%s:%s.mail", from, now)
-
-	file, err := os.Create(path.Join(mailDir, name))
-	if err != nil {
-		return err
-	}
-
-	writer := textproto.NewWriter(bufio.NewWriter(file))
-	for k, v := range mail.Header {
-		writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
-	}
-	writer.PrintfLine("")
-	file.Write(content)
-
-	return file.Close()
-}
-
-
-func (f RawHandler) SaveSent(email interfaces.Email) error {
-	mailDir := path.Join(f.root, email.From, email.Subject)
-	os.MkdirAll(mailDir, os.ModeDir|0o700)
-
-	file, err := os.Create(path.Join(mailDir, email.ID))
-	if err != nil {
-		println("file create", err.Error())
-		return err
-	}
-	defer file.Close()
-
-	_, err = file.Write(email.Raw)
-	if err != nil {
-		println("file write", err.Error())
-		return err
-	}
-
-	return nil
-}
-
-func (h RawHandler) 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:   h.domain,
-			Selector: "dkim",
-			Signer:   h.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.Bytes(),
-		)
-		if err != nil {
-			return err
-		}
-
-	}
-	return h.Save(from, to, raw)
-}
-
-func (f RawHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {
-	fmt.Println("mailer mailboxes for", folder)
-	dir, err := os.ReadDir(path.Join(f.root, folder))
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var threads []interfaces.Mailbox
-	for _, d := range dir {
-		if !d.IsDir() {
-			continue
-		}
-
-		info, err := d.Info()
-		if err != nil {
-			fmt.Println("dir info", err.Error())
-			continue
-		}
-		t := interfaces.Mailbox{
-			Title:   d.Name(),
-			LastMod: info.ModTime(),
-		}
-
-		threads = append(threads, t)
-	}
-	sort.Slice(
-		threads,
-		func(i, j int) bool {
-			return threads[i].LastMod.After(threads[j].LastMod)
-		},
-	)
-
-	return threads, nil
-}
-
-func (f RawHandler) Mails(folder string) ([]interfaces.Email, error) {
-	fmt.Println("mailer mails for", folder)
-
-	dir := path.Join(f.root, folder)
-	threadDir, err := os.ReadDir(dir)
-	if err != nil {
-		fmt.Println("readDir error:", err.Error())
-		return nil, err
-	}
-
-	var emails []interfaces.Email
-	for _, t := range threadDir {
-		mail, err := f.Mail(path.Join(folder, t.Name()))
-		if err != nil {
-			fmt.Println("mail error:", err.Error())
-			continue
-		}
-
-		emails = append(emails, mail)
-	}
-	sort.Slice(
-		emails,
-		func(i, j int) bool {
-			return emails[i].Date.Before(emails[j].Date)
-		},
-	)
-	fmt.Println("found", len(emails), "emails")
-
-	return emails, err
-}
-
-func (f RawHandler) Mail(file string) (interfaces.Email, error) {
-	fmt.Println("mailer getting", file)
-
-	mailFile, err := os.Open(path.Join(f.root, file))
-	if err != nil {
-		fmt.Println("open mail error:", err.Error())
-		return interfaces.Email{}, err
-	}
-	defer mailFile.Close()
-
-	mail, err := parsemail.Parse(mailFile)
-	if err != nil {
-		fmt.Println("email parse error:", err.Error())
-		return interfaces.Email{}, err
-	}
-
-	// try to decrypt
-	if f.privateKey != nil && pgp.IsPGPMessage(mail.TextBody) {
-		txt, err := helper.DecryptMessageArmored(f.keyText, nil, mail.TextBody)
-		if err != nil {
-			println(err)
-		}
-		mail.TextBody = txt
-	}
-	return interfaces.ToEmail(mail), nil
-}
-
-// Move is used to move messages between folders. The id parameter
-// is the message id to be moved and to is the destination folder.
-// Root is added automatically.
-func (f RawHandler) Move(id, to string) error {
-	return os.Rename(path.Join(f.root, id), path.Join(f.root, to, "/"))
-}
-
-// Delete is used to delete messages. The id parameter
-// is the message id to be deleted.
-// Root is added automatically.
-func (f RawHandler) Delete(id string) error {
-	return os.Remove(path.Join(f.root, id))
-}
-

model/file/file.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/model/file/file.go b/model/file/file.go
new file mode 100644
index 0000000..755c245
--- /dev/null
+++ b/model/file/file.go
@@ -0,0 +1,72 @@
+// file adds the common email file handling, but with a twist: here we save
+// emails first by inbox and then subject. This package also delivers some
+// functions to be used in the templates, for the web interface.
+package file
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+
+	"git.derelict.garden/dovel/email/model"
+
+	"blmayer.dev/x/vault"
+
+	"github.com/OfimaticSRL/parsemail"
+)
+
+// FileHandler is used to configure the file email handler.
+// Root is where mail should be saved, Templates is an optional field that
+// is the path to templates, to be used in the web server; Password is the
+// password for the x user, for now this is very simple; Domain is used to
+// filter and separate emails, only emails sent to one of your domains are
+// saved, each according to its configuration.
+type FileHandler struct {
+	root       string
+	domain     string
+	privateKey string
+	vault      vault.Vault[model.WebUser]
+}
+
+func NewFileHandler(c model.CommonConfig, v vault.Vault[model.WebUser]) (FileHandler, error) {
+	f := FileHandler{root: c.Root, vault: v, domain: c.Domain}
+	var err error
+	if c.PrivateKey != "" {
+		key, err := ioutil.ReadFile(c.PrivateKey)
+		if err != nil {
+			return f, err
+		}
+		f.privateKey = string(key)
+	}
+
+	return f, err
+}
+
+func (f FileHandler) Save(from, to string, r io.Reader) error {
+	m, err := parsemail.Parse(r)
+	if err != nil {
+		println("mail parse", err.Error())
+		return err
+	}
+
+	email := model.ToEmail(m)
+	mailDir := path.Join(f.root, email.To[0], email.Subject)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+
+	file, err := os.Create(path.Join(mailDir, email.ID))
+	if err != nil {
+		println("file create", err.Error())
+		return err
+	}
+	defer file.Close()
+
+	_, err = file.Write(email.Raw)
+	if err != nil {
+		println("file write", err.Error())
+		return err
+	}
+
+	return nil
+}
+

model/gwi/gwi.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/model/gwi/gwi.go b/model/gwi/gwi.go
new file mode 100644
index 0000000..0373693
--- /dev/null
+++ b/model/gwi/gwi.go
@@ -0,0 +1,132 @@
+// gwi contains the email handler for interacting with gwi.
+package gwi
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+
+	"git.derelict.garden/dovel/email/model"
+
+	"blmayer.dev/x/vault"
+
+	"github.com/OfimaticSRL/parsemail"
+)
+
+// GWIHandler is used to configure the GWI interface for dovel. Root is the
+// same root as in GWI: the path to the repositories.
+// Commands lets you add functions that will run for each received email, the
+// 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
+	notify     bool
+	Commands   map[string]func(email model.Email) error
+	domain     string
+	privateKey string
+	vault      vault.Vault[model.WebUser]
+}
+
+func NewGWIHandler(c model.CommonConfig, vault vault.Vault[model.WebUser]) (GWIHandler, error) {
+	g := GWIHandler{root: c.Root, domain: c.Domain, vault: vault}
+	if c.PrivateKey != "" {
+		key, err := ioutil.ReadFile(c.PrivateKey)
+		if err != nil {
+			return g, err
+		}
+
+		g.privateKey = string(key)
+	}
+	return g, nil
+}
+
+// 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(from, to string, r io.Reader) error {
+	m, err := parsemail.Parse(r)
+	if err != nil {
+		println("mail parse", err.Error())
+		return err
+	}
+
+	email := model.ToEmail(m)
+	userRepoDomain := strings.Split(email.To[0], "@")
+	userRepo := strings.Split(userRepoDomain[0], "/")
+	if len(userRepo) != 2 {
+		println("received bad to", userRepo)
+		return fmt.Errorf("received bad to: %s", userRepo)
+	}
+
+	user, repo := userRepo[0], userRepo[1]
+	if _, err := os.Stat(path.Join(g.root, user, repo)); err != nil {
+		println("stat repo", err.Error())
+		return err
+	}
+
+	// split by subject
+	start := strings.Index(email.Subject, "[")
+	if start == -1 {
+		println("received bad subject", email.Subject)
+		return fmt.Errorf("Malformed subject field")
+	}
+	title := strings.TrimSpace(email.Subject[start:])
+
+	mailDir := path.Join(g.root, user, repo, "mail", title)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+
+	mailFile, err := os.Create(path.Join(mailDir, email.ID))
+	if err != nil {
+		println("create mail file", err.Error())
+		return err
+	}
+	defer mailFile.Close()
+
+	_, err = mailFile.Write(email.Raw)
+	if err != nil {
+		println("write mail file", err.Error())
+		return err
+	}
+
+	// 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")
+	//	}
+	//}()
+
+	// notify owner
+	owner := g.vault.GetUser(user)
+	if owner.Name == "" {
+		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 nil
+}
+

model/html/html.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/model/html/html.go b/model/html/html.go
new file mode 100644
index 0000000..b6bd6e0
--- /dev/null
+++ b/model/html/html.go
@@ -0,0 +1,164 @@
+// file adds the common email file handling, but with a twist: here we save
+// emails first by inbox and then subject. This package also delivers some
+// functions to be used in the templates, for the web interface.
+package html
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"path"
+	"strings"
+	"text/template"
+	"time"
+
+	"git.derelict.garden/dovel/email/model"
+	"git.derelict.garden/dovel/email/util"
+
+	"github.com/ProtonMail/gopenpgp/v2/helper"
+
+	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
+)
+
+// HTMLHandler is used to configure the file email handler.
+// Root is where mail should be saved, Templates is an optional field that
+// is the path to templates, to be used in the web server; Password is the
+// password for the x user, for now this is very simple; Domain is used to
+// filter and separate emails, only emails sent to one of your domains are
+// saved, each according to its configuration.
+type HTMLHandler struct {
+	root       string
+	domain     string
+	privateKey string
+	indexTpl   *template.Template
+	listTpl    *template.Template
+	mailTpl    *template.Template
+}
+
+func NewHTMLHandler(c model.HTMLConfig) (HTMLHandler, error) {
+	f := HTMLHandler{root: c.Root, domain: c.Domain}
+
+	var err error
+	f.mailTpl, err = template.ParseFiles(c.MailTpl)
+	if err != nil {
+		return f, err
+	}
+
+	if c.PrivateKey != "" {
+		key, err := ioutil.ReadFile(c.PrivateKey)
+		if err != nil {
+			return f, err
+		}
+		f.privateKey = string(key)
+	}
+	return f, nil
+}
+
+func (h HTMLHandler) Save(from, to string, r io.Reader) error {
+	mail, err := mail.ReadMessage(r)
+	if err != nil {
+		return err
+	}
+
+	content, err := ioutil.ReadAll(mail.Body)
+	if err != nil {
+		println("file read", err.Error())
+		return err
+	}
+
+	// try to decrypt
+	if h.privateKey != "" && pgp.IsPGPMessage(string(content)) {
+		txt, err := helper.DecryptMessageArmored(
+			h.privateKey,
+			nil,
+			string(content),
+		)
+		if err != nil {
+			println(err)
+		}
+		content = []byte(txt)
+	}
+
+	date := time.Now()
+	if dt, err := mail.Header.Date(); err != nil {
+		date = dt
+	}
+	subj := mail.Header.Get("Subject")
+	mailDir := path.Join(h.root, to, subj)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+	name := fmt.Sprintf("%s:%s.mail", from, date.Format(time.RFC3339))
+
+	file, err := os.Create(path.Join(mailDir, name))
+	if err != nil {
+		return err
+	}
+
+	writer := textproto.NewWriter(bufio.NewWriter(file))
+	for k, v := range mail.Header {
+		writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
+	}
+	writer.PrintfLine("")
+	file.Write(content)
+	file.Close()
+
+	payload := model.Email{
+		Headers: mail.Header,
+		Date:    date,
+		From:    from,
+		To:      mail.Header["To"],
+		Subject: subj,
+		Body:    string(content),
+	}
+	if mail.Header.Get("MIME-Version") != "" {
+		payload.Body, payload.Attachments, err = util.Decode(
+			mail.Header.Get("Content-Type"),
+			string(content),
+		)
+		if err != nil {
+			println("decode", err.Error())
+		}
+	}
+
+	htmlName := fmt.Sprintf("%s:%s.html", from, date.Format(time.RFC3339))
+	file, err = os.Create(path.Join(mailDir, htmlName))
+	if err != nil {
+		return err
+	}
+	err = h.mailTpl.Execute(file, payload)
+	if err != nil {
+		return err
+	}
+	return file.Close()
+
+	// TODO: Update index and lists
+}
+
+func (f HTMLHandler) Mailboxes(folder string) ([]model.Mailbox, error) {
+	return nil, nil
+}
+
+func (f HTMLHandler) Mails(folder string) ([]model.Email, error) {
+	return nil, nil
+}
+
+func (f HTMLHandler) Mail(file string) (model.Email, error) {
+	return model.Email{}, nil
+}
+
+// Move is used to move messages between folders. The id parameter
+// is the message id to be moved and to is the destination folder.
+// Root is added automatically.
+func (f HTMLHandler) Move(id, to string) error {
+	return nil
+}
+
+// Delete is used to delete messages. The id parameter
+// is the message id to be deleted.
+// Root is added automatically.
+func (f HTMLHandler) Delete(id string) error {
+	return nil
+}

model/main.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/model/main.go b/model/main.go
new file mode 100644
index 0000000..595ebac
--- /dev/null
+++ b/model/main.go
@@ -0,0 +1,164 @@
+// package model contains interfaces that are passed to the common
+// functions on the server and web projects. You can import this to use
+// on templates and handlers.
+package model
+
+import (
+	"crypto"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"time"
+
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+
+	"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
+	WebPort *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.
+// 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
+}
+
+type InboxConfig struct {
+	Handler string
+	HTMLConfig
+}
+
+type HTMLConfig struct {
+	CommonConfig
+	IndexTpl string
+	ListTpl  string
+	MailTpl  string
+}
+
+type tz struct {
+	Name   string
+	Offset int
+}
+
+type CommonConfig struct {
+	Domain     string
+	PrivateKey string
+	Root       string
+}
+
+type WebUser struct {
+	Name       string
+	Email      string
+	Password   string
+	TimeZone   tz
+	PrivateKey crypto.Signer
+}
+
+func (w WebUser) Login() string {
+	return w.Name
+}
+
+func (w WebUser) Pass() string {
+	return w.Password
+}
+
+type File struct {
+	*object.File
+	Size int64
+}
+
+type Info struct {
+	User    string
+	Repo    string
+	Ref     plumbing.Hash
+	RefName string
+	Args    string
+}
+
+type Mailbox struct {
+	Title   string
+	LastMod time.Time
+	Length  int
+}
+
+type Attachment struct {
+	Name        string
+	ContentType string
+	Data        []byte
+}
+
+type Opt struct {
+	Encrypt bool
+	Key     string
+}
+
+type Email struct {
+	Headers     map[string][]string
+	ID          string
+	From        string
+	To          []string
+	Cc          []string
+	BCc         []string
+	Date        time.Time
+	Subject     string
+	Body        string
+	Attachments map[string]Attachment
+	Raw         []byte
+}
+
+func ToEmail(mail parsemail.Email) Email {
+	m := Email{
+		Headers:     mail.Header,
+		From:        mail.From[0].Address,
+		To:          []string{},
+		Cc:          []string{},
+		Subject:     mail.Subject,
+		ID:          mail.MessageID,
+		Date:        mail.Date,
+		Body:        mail.TextBody,
+		Attachments: map[string]Attachment{},
+	}
+	for _, to := range mail.To {
+		m.To = append(m.To, to.Address)
+	}
+	for _, cc := range mail.Cc {
+		m.Cc = append(m.Cc, cc.Address)
+	}
+	for _, a := range mail.Attachments {
+		content, err := io.ReadAll(a.Data)
+		if err != nil {
+			fmt.Println("read attachment", err.Error())
+			continue
+		}
+
+		encContent := base64.StdEncoding.EncodeToString(content)
+		m.Attachments[a.Filename] = Attachment{
+			Name:        a.Filename,
+			ContentType: a.ContentType,
+			Data:        []byte(encContent),
+		}
+	}
+	return m
+}
+
+type Mailer interface {
+	Save(from, to string, raw io.Reader) error
+}

model/raw/raw.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/model/raw/raw.go b/model/raw/raw.go
new file mode 100644
index 0000000..d8b8e04
--- /dev/null
+++ b/model/raw/raw.go
@@ -0,0 +1,121 @@
+// web adds the common email file handling, but with a twist: here we save
+// emails first by inbox and then subject. This package also delivers some
+// functions to be used in the templates, for the web interface.
+package raw
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"path"
+	"strings"
+	"time"
+
+	"git.derelict.garden/dovel/email/model"
+
+	pgp "github.com/ProtonMail/gopenpgp/v2/crypto"
+	"github.com/ProtonMail/gopenpgp/v2/helper"
+)
+
+// EawHandler is used to configure the raw email handler.
+// Root is where mail should be saved, Templates is an optional field that
+// is the path to templates, to be used in the web server; Password is the
+// password for the x user, for now this is very simple; Domain is used to
+// filter and separate emails, only emails sent to one of your domains are
+// saved, each according to its configuration.
+type RawHandler struct {
+	root       string
+	domain     string
+	privateKey string
+}
+
+func NewRawHandler(c model.CommonConfig) (RawHandler, error) {
+	h := RawHandler{root: c.Root, domain: c.Domain}
+
+	if c.PrivateKey != "" {
+		key, err := ioutil.ReadFile(c.PrivateKey)
+		if err != nil {
+			return h, err
+		}
+		h.privateKey = string(key)
+	}
+
+	return h, nil
+}
+
+func (h RawHandler) Save(from, to string, raw io.Reader) error {
+	mail, err := mail.ReadMessage(raw)
+	if err != nil {
+		return err
+	}
+
+	content, err := ioutil.ReadAll(mail.Body)
+	if err != nil {
+		println("file read", err.Error())
+		return err
+	}
+
+	// try to decrypt
+	if h.privateKey != "" && pgp.IsPGPMessage(string(content)) {
+		println("decrypting")
+		txt, err := helper.DecryptMessageArmored(
+			h.privateKey,
+			nil,
+			string(content),
+		)
+		if err != nil {
+			println(err)
+		}
+		content = []byte(txt)
+	}
+
+	now := time.Now().Format(time.RFC3339)
+	subj := mail.Header.Get("Subject")
+	mailDir := path.Join(h.root, to, subj)
+	os.MkdirAll(mailDir, os.ModeDir|0o700)
+	name := fmt.Sprintf("%s:%s.mail", from, now)
+
+	file, err := os.Create(path.Join(mailDir, name))
+	if err != nil {
+		return err
+	}
+
+	writer := textproto.NewWriter(bufio.NewWriter(file))
+	for k, v := range mail.Header {
+		writer.PrintfLine("%s: %s", k, strings.Join(v, ", "))
+	}
+	writer.PrintfLine("")
+	file.Write(content)
+
+	return file.Close()
+}
+
+func (f RawHandler) Mailboxes(folder string) ([]model.Mailbox, error) {
+	return nil, nil
+}
+
+func (f RawHandler) Mails(folder string) ([]model.Email, error) {
+	return nil, nil
+}
+
+func (f RawHandler) Mail(file string) (model.Email, error) {
+	return model.Email{}, nil
+}
+
+// Move is used to move messages between folders. The id parameter
+// is the message id to be moved and to is the destination folder.
+// Root is added automatically.
+func (f RawHandler) Move(id, to string) error {
+	return nil
+}
+
+// Delete is used to delete messages. The id parameter
+// is the message id to be deleted.
+// Root is added automatically.
+func (f RawHandler) Delete(id string) error {
+	return nil
+}

util/decode/multipart.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/util/decode/multipart.go b/util/decode/multipart.go
new file mode 100644
index 0000000..2df6798
--- /dev/null
+++ b/util/decode/multipart.go
@@ -0,0 +1,11 @@
+package util
+
+import "strings"
+
+func Decode(header, content string) (string, map[string]string) {
+	if strings.Contains(header, "multipart") {
+		boundary := strings.Split(header, `"`)[1]
+	}
+
+
+}

util/multipart.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/util/multipart.go b/util/multipart.go
new file mode 100644
index 0000000..698bb27
--- /dev/null
+++ b/util/multipart.go
@@ -0,0 +1,61 @@
+package util
+
+import (
+	"encoding/base64"
+	"io"
+	"mime"
+	"mime/multipart"
+	"strings"
+
+	"git.derelict.garden/dovel/email/model"
+)
+
+func Decode(header, content string) (string, map[string]model.Attachment, error) {
+	mediaType, params, err := mime.ParseMediaType(header)
+	if err != nil {
+		return content, nil, err
+	}
+	if !strings.HasPrefix(mediaType, "multipart/") {
+		return content, nil, nil
+	}
+
+	var text string
+	attach := map[string]model.Attachment{}
+	r := multipart.NewReader(strings.NewReader(content), params["boundary"])
+	for {
+		p, err := r.NextPart()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return text, attach, err
+		}
+		cont, err := io.ReadAll(p)
+		if err != nil {
+			return text, attach, err
+		}
+		
+		if p.Header.Get("Content-Transfer-Encoding") == "base64" {
+			cont, err = base64.RawStdEncoding.DecodeString(
+				string(cont),
+			)
+			if err != nil {
+				println("base64", err.Error())
+			}
+		}
+
+		cType := p.Header.Get("Content-Type")
+		if fileName := p.FileName(); fileName != "" { 
+			attach[fileName] = model.Attachment{
+				Name: fileName,
+				Data: cont,
+				ContentType: cType,
+			}
+			continue
+		}
+		if strings.HasPrefix(cType, "text/plain") {
+			text = string(cont)
+		}
+	}
+	return text, attach, nil
+}

util/multipart_test.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/util/multipart_test.go b/util/multipart_test.go
new file mode 100644
index 0000000..2ea92f0
--- /dev/null
+++ b/util/multipart_test.go
@@ -0,0 +1,32 @@
+package util
+
+import (
+	"testing"
+)
+
+var header = `multipart/alternative; boundary="000000000000b05aa10600f3345f"`
+var body = `--000000000000b05aa10600f3345f
+Content-Type: text/plain; charset="UTF-8"
+
+sdfv qergwergwergvd fvds fvv
+
+--000000000000b05aa10600f3345f
+Content-Type: text/html; charset="UTF-8"
+
+<div dir="ltr"><div class="gmail_default" style="font-family:arial,sans-serif;font-size:small;color:#000000">sdfv qergwergwergvd fvds fvv<br></div></div>
+
+--000000000000b05aa10600f3345f--
+`
+
+func TestDecode(t *testing.T) {
+	text, attach, err := Decode(header, body)
+	if err != nil {
+		t.Error(err)
+	}
+	if text == "" {
+		t.Error("text is empty")
+	}
+	t.Log(text)
+	t.Log(attach)
+}
+

util/parse_test.go

commit c18a9369905bb8a91c13778037afe014fb8dcfc8
Author: b <git@mail.blmayer.dev>
Date:   Thu Jul 20 20:35:59 2023 -0300

    Added html handler
    
    - Removed web part
    - Renamed interfaces to model
    - Moved sending email to main
    - Improved configs

diff --git a/util/parse_test.go b/util/parse_test.go
new file mode 100644
index 0000000..59d917d
--- /dev/null
+++ b/util/parse_test.go
@@ -0,0 +1,28 @@
+package util
+
+import (
+	"testing"
+
+	"git.derelict.garden/dovel/email/model"
+	"git.derelict.garden/dovel/email/model/file"
+)
+
+func TestParseMail(t *testing.T) {
+	h, err := file.NewFileHandler(model.CommonConfig{}, nil, nil)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	mail, err := h.Mail(
+		"mail/test/attachment/base64.txt",
+	)
+	if err != nil {
+		t.Error(err)
+	}
+	if len(mail.Attachments) == 0 {
+		t.Error("attachment not parsed")
+	}
+
+	t.Log(mail)
+}