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

163adfe

Author: blmayer (bleemayer@gmail.com)

Date: Sat Apr 22 22:06:17 2023 -0300

Parent: 9c123fb

Refactored backend and handlers

- Also did a small style on pages

Diff

cmd/dovel/backend.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/cmd/dovel/backend.go b/cmd/dovel/backend.go
index 1a07252..cd42039 100644
--- a/cmd/dovel/backend.go
+++ b/cmd/dovel/backend.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"strings"
 
-	"blmayer.dev/x/dovel/config"
 	"blmayer.dev/x/dovel/interfaces"
 	"github.com/OfimaticSRL/parsemail"
 	"github.com/emersion/go-smtp"
@@ -15,7 +14,7 @@ import (
 // A Session is returned after EHLO.
 type Session struct {
 	User     string
-	handlers map[string]config.Handler
+	handlers map[string]interfaces.Mailer
 }
 
 func (s Session) AuthPlain(username, password string) error {
@@ -57,7 +56,7 @@ func (s Session) Data(r io.Reader) error {
 			return fmt.Errorf("no handler for domain %s", userDomain)
 		}
 		mail.To = []string{to}
-		if err := handler(mail); err != nil {
+		if err := handler.Save(mail); err != nil {
 			println("handler error", err.Error())
 			return fmt.Errorf("handler error %s", err.Error())
 		}
@@ -74,7 +73,7 @@ func (s Session) Logout() error {
 }
 
 type backend struct {
-	Handlers   map[string]config.Handler
+	Handlers   map[string]interfaces.Mailer
 	PrivateKey crypto.Signer
 }
 

cmd/dovel/main.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index 9c4edc1..6f37484 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -6,9 +6,11 @@ import (
 	"net/http"
 	"os"
 	"path"
+	"html/template"
 	"time"
 
 	"blmayer.dev/x/dovel/config"
+	"blmayer.dev/x/dovel/interfaces"
 	"blmayer.dev/x/dovel/interfaces/file"
 	"blmayer.dev/x/dovel/interfaces/gwi"
 
@@ -34,7 +36,7 @@ var (
 		},
 	}
 	cfg config.Config
-	b   = backend{Handlers: map[string]config.Handler{}}
+	b   = backend{Handlers: map[string]interfaces.Mailer{}}
 )
 
 func main() {
@@ -60,7 +62,7 @@ func main() {
 				panic(err)
 			}
 
-			b.Handlers[hand.Domain] = g.Save
+			b.Handlers[hand.Domain] = g
 		case "file":
 			v, err := NewPlainTextVault(path.Join(hand.Root, "users.json"))
 			if err != nil {
@@ -72,13 +74,22 @@ func main() {
 				panic(err)
 			}
 
+			web := webHandler{
+				assetsPath: hand.Templates,
+				vault:      v,
+				mailer:     mail,
+			}
 			if hand.Templates != "" {
-				http.HandleFunc(hand.Domain+"/", mail.IndexHandler())
-				http.HandleFunc(hand.Domain+"/assets/", mail.AssetsHandler())
-				http.HandleFunc(hand.Domain+"/out", SendHandler(mail, v))
+				web.templates, err = template.ParseGlob(hand.Templates + "/*.html")
+				if err != nil {
+					panic(err)
+				}
+				http.HandleFunc(hand.Domain+"/", web.indexHandler())
+				http.HandleFunc(hand.Domain+"/assets/", web.assetsHandler())
+				http.HandleFunc(hand.Domain+"/out", web.sendHandler())
 			}
 
-			b.Handlers[hand.Domain] = mail.Save
+			b.Handlers[hand.Domain] = mail
 		}
 		println("added handler " + hand.Domain + " as " + hand.Handler)
 	}

cmd/dovel/web.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/cmd/dovel/web.go b/cmd/dovel/web.go
index cdc925f..d7bb4b7 100644
--- a/cmd/dovel/web.go
+++ b/cmd/dovel/web.go
@@ -1,18 +1,63 @@
 package main
 
 import (
+	"html/template"
 	"io"
 	"net/http"
+	"net/url"
+	"path"
 	"time"
 
 	"blmayer.dev/x/dovel/interfaces"
 )
 
-func SendHandler(mailer interfaces.Mailer, vault interfaces.Vault) http.HandlerFunc {
+type webHandler struct {
+	assetsPath string
+	templates  *template.Template
+	vault      interfaces.Vault
+	mailer     interfaces.Mailer
+}
+
+func (h webHandler) indexHandler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		user, pass, _ := r.BasicAuth()
+		if user == "" || !h.vault.Validate(user, pass) {
+			w.Header().Add("WWW-Authenticate", "Basic")
+			http.Error(w, "wrong auth", http.StatusUnauthorized)
+			return
+		}
+
+		if r.URL.Path == "/" {
+			r.URL.Path = "/index.html"
+		}
+
+		err := h.templates.ExecuteTemplate(
+			w, r.URL.Path[1:],
+			struct {
+				Query  url.Values
+				Mailer interfaces.Mailer
+			}{
+				r.URL.Query(),
+				h.mailer,
+			},
+		)
+		if err != nil {
+			println("execute", err.Error())
+		}
+	}
+}
+
+func (h webHandler) assetsHandler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, path.Join(h.assetsPath, r.URL.Path[1:]))
+	}
+}
+
+func (h webHandler) sendHandler() http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		println("file handling send email")
 		user, pass, _ := r.BasicAuth()
-		if user == "" || !vault.Validate(user, pass) {
+		if user == "" || !h.vault.Validate(user, pass) {
 			w.Header().Add("WWW-Authenticate", "Basic")
 			http.Error(w, "wrong auth", http.StatusUnauthorized)
 			return
@@ -54,8 +99,8 @@ func SendHandler(mailer interfaces.Mailer, vault interfaces.Vault) http.HandlerF
 				// TODO: Add content-type
 			}
 		}
-		
-		err := mailer.Send(email)
+
+		err := h.mailer.Send(email)
 		if err != nil {
 			println("send error: " + err.Error())
 			http.Error(w, "send error"+err.Error(), http.StatusInternalServerError)
@@ -64,4 +109,3 @@ func SendHandler(mailer interfaces.Mailer, vault interfaces.Vault) http.HandlerF
 		http.Redirect(w, r, "/", http.StatusFound)
 	}
 }
-

config/config.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/config/config.go b/config/config.go
index f3c21ab..0e23958 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,13 +1,5 @@
 package config
 
-import (
-	"blmayer.dev/x/dovel/interfaces"
-)
-
-// Handler is the way to add custom logic to the email handlers, after an
-// email is received and saved all handlers are run
-type Handler func(email interfaces.Email) error
-
 // 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

interfaces/file/file.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
index a987c0f..728b8f4 100644
--- a/interfaces/file/file.go
+++ b/interfaces/file/file.go
@@ -10,12 +10,10 @@ import (
 	"encoding/base64"
 	"encoding/pem"
 	"fmt"
-	"html/template"
 	"io"
 	"io/ioutil"
 	"mime/multipart"
 	"net"
-	"net/http"
 	"net/smtp"
 	"os"
 	"path"
@@ -37,9 +35,7 @@ import (
 // filter and separate emails, only emails sent to one of your domains are
 // saved, each according to its configuration.
 type FileHandler struct {
-	templates  *template.Template
 	root       string
-	assetsPath string
 	domain     string
 	privateKey crypto.Signer
 	vault      interfaces.Vault
@@ -55,12 +51,6 @@ func NewFileHandler(c config.InboxConfig, v interfaces.Vault, fs map[string]any)
 	fs["mail"] = f.Mail
 
 	var err error
-	if c.Templates != "" {
-		f.assetsPath = c.Templates
-		f.templates, err = template.New(c.Root).Funcs(fs).
-			ParseGlob(c.Templates + "/*.html")
-	}
-
 	if c.DKIMKeyPath != "" {
 		key, err := ioutil.ReadFile(c.DKIMKeyPath)
 		if err != nil {
@@ -81,34 +71,6 @@ func NewFileHandler(c config.InboxConfig, v interfaces.Vault, fs map[string]any)
 	return f, err
 }
 
-func (f FileHandler) IndexHandler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		user, pass, _ := r.BasicAuth()
-		if user == "" || !f.vault.Validate(user, pass) {
-			w.Header().Add("WWW-Authenticate", "Basic")
-			http.Error(w, "wrong auth", http.StatusUnauthorized)
-			return
-		}
-
-		if r.URL.Path == "/" {
-			r.URL.Path = "/index.html"
-		}
-
-		err := f.templates.ExecuteTemplate(
-			w, r.URL.Path[1:],
-			r.URL.Query(),
-		)
-		if err != nil {
-			println("execute", err.Error())
-		}
-	}
-}
-
-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) Save(email interfaces.Email) error {
 	mailDir := path.Join(f.root, email.To[0], email.Subject)

interfaces/gwi/gwi.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
index c945455..87ab1ab 100644
--- a/interfaces/gwi/gwi.go
+++ b/interfaces/gwi/gwi.go
@@ -5,19 +5,23 @@ import (
 	"bytes"
 	"crypto"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/pem"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"mime/multipart"
 	"net"
 	"net/smtp"
 	"os"
 	"path"
+	"sort"
 	"strings"
 	"time"
 
 	"blmayer.dev/x/dovel/config"
 	"blmayer.dev/x/dovel/interfaces"
+	"github.com/OfimaticSRL/parsemail"
 	"github.com/emersion/go-msgauth/dkim"
 )
 
@@ -27,7 +31,7 @@ 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
+	root       string
 	Commands   map[string]func(email interfaces.Email) error
 	domain     string
 	privateKey crypto.Signer
@@ -35,7 +39,7 @@ type GWIHandler struct {
 }
 
 func NewGWIHandler(c config.InboxConfig, vault interfaces.Vault) (GWIHandler, error) {
-	g := GWIHandler{Root: c.Root, domain: c.Domain, vault: vault}
+	g := GWIHandler{root: c.Root, domain: c.Domain, vault: vault}
 	if c.DKIMKeyPath != "" {
 		key, err := ioutil.ReadFile(c.DKIMKeyPath)
 		if err != nil {
@@ -66,7 +70,7 @@ func (g GWIHandler) Save(email interfaces.Email) error {
 	}
 
 	user, repo := userRepo[0], userRepo[1]
-	if _, err := os.Stat(path.Join(g.Root, user, repo)); err != nil {
+	if _, err := os.Stat(path.Join(g.root, user, repo)); err != nil {
 		println("stat repo", err.Error())
 		return err
 	}
@@ -79,7 +83,7 @@ func (g GWIHandler) Save(email interfaces.Email) error {
 	}
 	title := strings.TrimSpace(email.Subject[start:])
 
-	mailDir := path.Join(g.Root, user, repo, "mail", title)
+	mailDir := path.Join(g.root, user, repo, "mail", title)
 	os.MkdirAll(mailDir, os.ModeDir|0o700)
 
 	mailFile, err := os.Create(path.Join(mailDir, email.ID))
@@ -213,3 +217,121 @@ func (g GWIHandler) Send(mail interfaces.Email) error {
 	}
 	return nil
 }
+
+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
+}

interfaces/main.go

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/interfaces/main.go b/interfaces/main.go
index 09300a2..03e74a4 100644
--- a/interfaces/main.go
+++ b/interfaces/main.go
@@ -83,5 +83,8 @@ func ToEmail(mail parsemail.Email) Email {
 type Mailer interface {
 	Send(mail Email) error
 	Save(mail Email) error
+	Mailboxes(folder string) ([]Mailbox, error)
+	Mails(folder string) ([]Email, error)
+	Mail(file string) (Email, error)
 }
 

www/compose.html

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/www/compose.html b/www/compose.html
index ff0b41a..24d96b0 100644
--- a/www/compose.html
+++ b/www/compose.html
@@ -5,11 +5,11 @@
 	<table>
 		<tr>
 			<td style="padding-left: 0"><label for="from">From: </label></td>
-			<td><input style="width:100%" name=from {{if .inbox}}value="{{index .inbox 0}}"{{end}}></td>
+			<td><input style="width:100%" name=from {{if .Query.inbox}}value="{{.Query.Get "inbox" }}"{{end}}></td>
 		</tr>
 		<tr>
 			<td style="padding-left: 0"><label>To: </label></td>
-			<td><input style="width:100%" name=to {{if .to}}value="{{index .to 0}}"{{end}}></td>
+			<td><input style="width:100%" name=to {{if .Query.to}}value="{{.Query.Get "to"}}"{{end}}></td>
 		</tr>
 		<tr>
 			<td style="padding-left: 0"><label>CC: </label></td>
@@ -21,7 +21,7 @@
 		</tr>
 		<tr>
 			<td style="padding-left: 0"><label>Subject: </label></td>
-			<td><input style="width:100%" name=subject {{if .subj}}value="{{index .subj 0}}"{{end}}></td>
+			<td><input style="width:100%" name=subject {{if .Query.subj}}value="{{.Query.Get "subj"}}"{{end}}></td>
 		</tr>
 	</table>
 	<field><textarea style="width:100%" name=body rows="7" cols="50" placeholder="Body:"></textarea><br></field>

www/inboxes.html

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/www/inboxes.html b/www/inboxes.html
index 0b82ec3..3bbf0a6 100644
--- a/www/inboxes.html
+++ b/www/inboxes.html
@@ -1,12 +1,12 @@
 {{template "head.html"}}
 
-<b>inbox {{.Get "inbox"}}</b><br><br>
+<h2>{{.Query.Get "inbox"}}</h2>
 <a href="index.html">index</a>&emsp;
-<a href="compose.html?inbox={{.Get "inbox"}}">compose</a>
+<a href="compose.html?inbox={{.Query.Get "inbox"}}">compose</a>
 <br><br>
 
-{{range (inboxes (.Get "inbox"))}}
-	<a href="mails.html?inbox={{$.Get "inbox"}}&subj={{.Title}}">{{.Title}}</a><br>
+{{range (.Mailer.Mailboxes (.Query.Get "inbox"))}}
+	<a href="mails.html?inbox={{$.Query.Get "inbox"}}&subj={{.Title}}">{{.Title}}</a><br>
 	Updated: {{.LastMod.Format "Mon, 02 Jan 2006 15:04:05 MST"}}<br>
 	<br>
 {{end}}

www/index.html

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/www/index.html b/www/index.html
index 5521891..5d17e43 100644
--- a/www/index.html
+++ b/www/index.html
@@ -16,13 +16,11 @@ _  __  /_  __ \_ | / /  _ \_  /
 
 </pre>
 	</center>
-<b>inboxes!</b><br>
-<br>
 
 <a href=compose.html>compose</a><br>
 <br>
 
-{{range (inboxes ".")}}
+{{range (.Mailer.Mailboxes ".")}}
 	<a href="inboxes.html?inbox={{.Title}}">{{.Title}}</a><br>
 	Updated: {{.LastMod.Format "Mon, 02 Jan 2006 15:04:05 MST"}}<br>
 	<br>

www/mails.html

commit 163adfe2975af375af47a45c1009026ab1f40dee
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Apr 22 22:06:17 2023 -0300

    Refactored backend and handlers
    
    - Also did a small style on pages

diff --git a/www/mails.html b/www/mails.html
index 235b2c0..c9a8f27 100644
--- a/www/mails.html
+++ b/www/mails.html
@@ -1,12 +1,11 @@
 {{template "head.html"}}
-<b>about {{.Get "subj"}}</b><br>
-<br>
-<a href="inboxes.html?inbox={{.Get "inbox"}}">inbox</a>&emsp;
-<a href="compose.html?inbox={{.Get "inbox"}}&subj={{.Get "subj"}}">compose</a><br>
-
-{{range (mails (print (.Get "inbox") "/" (.Get "subj")))}}
+<h2>{{.Query.Get "subj"}}</h2>
+<a href="inboxes.html?inbox={{.Query.Get "inbox"}}">inbox</a>&emsp;
+<a href="compose.html?inbox={{.Query.Get "inbox"}}&subj={{.Query.Get "subj"}}">compose</a><br>
+{{$inbox := printf "%s/%s" (.Query.Get "inbox") (.Query.Get "subj")}}
+{{range (.Mailer.Mails $inbox)}}
 <p>
-	{{if eq ($.Get "inbox") .From}}To: {{index .To 0}}{{else}}From: {{.From}}{{end}}<br>
+	{{if eq ($.Query.Get "inbox") .From}}To: {{index .To 0}}{{else}}From: {{.From}}{{end}}<br>
 	Date: {{.Date.Format "Mon, 02 Jan 2006 15:04:05 MST"}}<br>
 	{{if .Cc}}Cc: .Cc<br>{{end}}
 	{{if .Attachments}}
@@ -18,6 +17,6 @@
 	{{if .Body}}
 	<pre>{{.Body}}</pre>
 	{{end}}
-	<a href="compose.html?inbox={{$.Get "inbox"}}&subj={{.Subject}}&to={{.From}}">reply</a>
+	<a href="compose.html?inbox={{$.Query.Get "inbox"}}&subj={{.Subject}}&to={{.From}}">reply</a>
 </p>
 {{end}}