This is the main dovel repository, it has the Go code to run dovel SMTP server.
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
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 }
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) }
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) } } -
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
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)
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 +}
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) }
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>
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>  -<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}}
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>
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>  -<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>  +<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}}