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