This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Fri Sep 8 02:05:56 2023 -0300
Parent: 95eb49e
Removed handlers
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/Makefile b/Makefile index 4038943..44854dd 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean deploy deploy-site deploy-assets deploy-pages -dovel: $(wildcard cmd/dovel/*.go model/*/*.go model/*.go util/*/*.go) +dovel: $(wildcard cmd/dovel/*.go *.go util/*/*.go) GOARCH=arm GOARM=6 go build -o $@ cmd/dovel/*.go deploy: dovel
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go index b647637..6a95bf6 100644 --- a/cmd/dovel/main.go +++ b/cmd/dovel/main.go @@ -7,14 +7,14 @@ import ( "time" "git.derelict.garden/bryon/vault" - "git.derelict.garden/dovel/email/model" + "git.derelict.garden/dovel/email" "github.com/emersion/go-smtp" ) var ( domain string configPath string - v vault.Vault[model.WebUser] + v vault.Vault[email.WebUser] ) func main() { @@ -30,10 +30,10 @@ func main() { panic(err) } - cfg := model.Config{} + cfg := email.Config{} json.NewDecoder(configFile).Decode(&cfg) - v, err = vault.NewJSONPlainTextVault[model.WebUser](cfg.VaultFile) + v, err = vault.NewJSONPlainTextVault[email.WebUser](cfg.VaultFile) if err != nil { panic(err) }
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/model.go b/model.go new file mode 100644 index 0000000..dd0b2d7 --- /dev/null +++ b/model.go @@ -0,0 +1,132 @@ +// package dovel 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 email + +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 used to configure your email server. +// Port is used to specify which port the server should listen +// to connections, a typicall value is 2525. +// Domain is what the server should respond in a HELO or EHLO request. +// Handlers is a list of domains that are allowed to send/receive emails. +type Config struct { + Port string + Domain string + VaultFile string +} + +type tz struct { + Name string + Offset int +} + +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 +}
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/model/file/file.go b/model/file/file.go deleted file mode 100644 index 755c245..0000000 --- a/model/file/file.go +++ /dev/null @@ -1,72 +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 ( - "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 -} -
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/model/gwi/gwi.go b/model/gwi/gwi.go deleted file mode 100644 index 0373693..0000000 --- a/model/gwi/gwi.go +++ /dev/null @@ -1,132 +0,0 @@ -// 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 -} -
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/model/html/html.go b/model/html/html.go deleted file mode 100644 index 0c24694..0000000 --- a/model/html/html.go +++ /dev/null @@ -1,290 +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 html - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "net/mail" - "net/textproto" - "os" - "path" - "sort" - "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 - out string - domain string - privateKey string - indexTpl *template.Template - listTpl *template.Template - mailsTpl *template.Template - mailTpl *template.Template -} - -func NewHTMLHandler(c model.HTMLConfig) (HTMLHandler, error) { - f := HTMLHandler{root: c.Root, domain: c.Domain, out: c.Out} - - var err error - f.mailTpl, err = template.ParseFiles(c.MailTpl) - if err != nil { - return f, err - } - f.mailsTpl, err = template.ParseFiles(c.MailsTpl) - if err != nil { - return f, err - } - f.listTpl, err = template.ParseFiles(c.ListTpl) - if err != nil { - return f, err - } - f.indexTpl, err = template.ParseFiles(c.IndexTpl) - 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()) - } - } - - htmlDir := path.Join(h.out, to, subj) - os.MkdirAll(htmlDir, os.ModeDir|0o700) - htmlName := fmt.Sprintf("%s:%s.html", from, date.Format(time.RFC3339)) - file, err = os.Create(path.Join(htmlDir, htmlName)) - if err != nil { - return err - } - err = h.mailTpl.Execute(file, payload) - if err != nil { - return err - } - file.Close() - - dir, err := os.ReadDir(path.Join(h.out)) - if err != nil { - fmt.Println("readDir error:", err.Error()) - return err - } - - var threads []struct { - Title string - LastMod time.Time - } - for _, d := range dir { - if !d.IsDir() { - continue - } - - info, err := d.Info() - if err != nil { - fmt.Println("dir info", err.Error()) - continue - } - t := struct { - Title string - LastMod time.Time - }{ - 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) - }, - ) - - file, err = os.Create(path.Join(h.out, "index.html")) - if err != nil { - return err - } - err = h.indexTpl.Execute(file, threads) - if err != nil { - return err - } - file.Close() - - dir, err = os.ReadDir(path.Join(h.out, to)) - if err != nil { - fmt.Println("readDir error:", err.Error()) - return err - } - - threads = []struct { - Title string - LastMod time.Time - }{} - for _, d := range dir { - if !d.IsDir() { - continue - } - - info, err := d.Info() - if err != nil { - fmt.Println("dir info", err.Error()) - continue - } - t := struct { - Title string - LastMod time.Time - }{ - 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) - }, - ) - - file, err = os.Create(path.Join(h.out, to, "index.html")) - if err != nil { - return err - } - err = h.listTpl.Execute(file, threads) - if err != nil { - return err - } - file.Close() - - dir, err = os.ReadDir(path.Join(h.out, to, subj)) - if err != nil { - fmt.Println("read subj dir error:", err.Error()) - return err - } - - threads = []struct { - Title string - LastMod time.Time - }{} - for _, d := range dir { - info, err := d.Info() - if err != nil { - fmt.Println("dir info", err.Error()) - continue - } - t := struct { - Title string - LastMod time.Time - }{ - Title: d.Name(), - LastMod: info.ModTime(), - } - - threads = append(threads, t) - } - sort.Slice( - threads, - func(i, j int) bool { - return threads[i].LastMod.Before(threads[j].LastMod) - }, - ) - - file, err = os.Create(path.Join(h.out, to, subj, "index.html")) - if err != nil { - return err - } - err = h.mailsTpl.Execute(file, threads) - if err != nil { - return err - } - return file.Close() -}
commit d4e60c7a37f4552dd4eacfc2e98144f378bcc51f Author: blmayer <bleemayer@gmail.com> Date: Fri Sep 8 02:05:56 2023 -0300 Removed handlers diff --git a/model/raw/raw.go b/model/raw/raw.go deleted file mode 100644 index d8b8e04..0000000 --- a/model/raw/raw.go +++ /dev/null @@ -1,121 +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" - "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 -}