This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Tue Apr 18 19:15:34 2023 -0300
Parent: 2abc125
Improved sending email
commit e737530dfcc9a336d95519924f6a13f301720e10 Author: blmayer <bleemayer@gmail.com> Date: Tue Apr 18 19:15:34 2023 -0300 Improved sending email diff --git a/TODO.txt b/TODO.txt index d714e35..4761707 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,8 @@ ☐ Support PGP ✓ Support DKIM if needed ☐ Improve web users +☐ Add insert attachments button +☐ Use XDG desktop for config path ☐ Support SMTP for sending email? ☐ Add config menu ☐ Add denylist filtering
commit e737530dfcc9a336d95519924f6a13f301720e10 Author: blmayer <bleemayer@gmail.com> Date: Tue Apr 18 19:15:34 2023 -0300 Improved sending email diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go index 105d18f..4d435d8 100644 --- a/cmd/dovel/main.go +++ b/cmd/dovel/main.go @@ -52,7 +52,7 @@ func main() { switch hand.Handler { case "gwi": // load gwi user file - v, err := NewPGPVault(path.Join(hand.Root, "users.json")) + v, err := NewPlainTextVault(path.Join(hand.Root, "users.json")) if err != nil { panic(err) } @@ -63,15 +63,20 @@ func main() { b.Handlers[hand.Domain] = g.Save case "file": + v, err := NewPlainTextVault(path.Join(hand.Root, "users.json")) + if err != nil { + panic(err) + } + funcs := map[string]any{"heading": heading} - mail, err := file.NewFileHandler(hand, funcs) + mail, err := file.NewFileHandler(hand, v, funcs) if err != nil { panic(err) } if hand.Templates != "" { http.HandleFunc(hand.Domain+"/", mail.IndexHandler()) - http.HandleFunc(hand.Domain+"/assets/", newAssetsHandler(hand.Templates)) + http.HandleFunc(hand.Domain+"/assets/", mail.AssetsHandler()) http.HandleFunc(hand.Domain+"/out", mail.SendHandler(b)) } @@ -100,14 +105,8 @@ func main() { }() } - println("Starting mail server at", s.Addr) + println("starting mail server at", s.Addr) if err := s.ListenAndServe(); err != nil { log.Fatal(err) } } - -func newAssetsHandler(root string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, path.Join(root, r.URL.Path[1:])) - } -}
commit e737530dfcc9a336d95519924f6a13f301720e10 Author: blmayer <bleemayer@gmail.com> Date: Tue Apr 18 19:15:34 2023 -0300 Improved sending email diff --git a/cmd/dovel/vault.go b/cmd/dovel/vault.go index 1cc6d12..d8b7a72 100644 --- a/cmd/dovel/vault.go +++ b/cmd/dovel/vault.go @@ -7,30 +7,30 @@ import ( "blmayer.dev/x/dovel/interfaces" ) -type pgpUser struct { - Name string - PGP string - Address string +type plainTextUser struct { + Name string + Address string + Password string } -func (u pgpUser) Email() string { +func (u plainTextUser) Email() string { return u.Address } -func (u pgpUser) Login() string { +func (u plainTextUser) Login() string { return u.Name } -func (u pgpUser) Pass() string { - return u.PGP +func (u plainTextUser) Pass() string { + return u.Password } -type pgpVault struct { - Users []pgpUser +type plainTextVault struct { + Users []plainTextUser } -func NewPGPVault(path string) (interfaces.Vault, error) { - s := pgpVault{Users: []pgpUser{}} +func NewPlainTextVault(path string) (interfaces.Vault, error) { + s := plainTextVault{Users: []plainTextUser{}} file, err := os.Open(path) if err != nil { @@ -46,7 +46,7 @@ func NewPGPVault(path string) (interfaces.Vault, error) { return s, nil } -func (f pgpVault) GetUser(login string) interfaces.User { +func (f plainTextVault) GetUser(login string) interfaces.User { for _, u := range f.Users { if u.Name == login { return u @@ -56,6 +56,13 @@ func (f pgpVault) GetUser(login string) interfaces.User { } // Validate is not used here. Only here to fill the interface -func (f pgpVault) Validate(login, pass string) bool { - return false +func (f plainTextVault) Validate(login, pass string) bool { + user := f.GetUser(login) + if user == nil { + return false + } + if user.Pass() != pass { + return false + } + return true }
commit e737530dfcc9a336d95519924f6a13f301720e10 Author: blmayer <bleemayer@gmail.com> Date: Tue Apr 18 19:15:34 2023 -0300 Improved sending email diff --git a/interfaces/backend/backend.go b/interfaces/backend/backend.go index 6348a25..9454180 100644 --- a/interfaces/backend/backend.go +++ b/interfaces/backend/backend.go @@ -4,21 +4,14 @@ package backend import ( - "bytes" "crypto" "fmt" "io" - "mime/multipart" - "net" - "net/http" - "strconv" "strings" - "time" "blmayer.dev/x/dovel/config" "blmayer.dev/x/dovel/interfaces" "github.com/OfimaticSRL/parsemail" - "github.com/emersion/go-msgauth/dkim" "github.com/emersion/go-smtp" ) @@ -92,127 +85,3 @@ func (b Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { return Session{handlers: b.Handlers}, nil } -func (b Backend) SendHandler(saveFunction config.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - println("backend sending email") - if err := r.ParseMultipartForm(20 * 1024 * 1024); err != nil { - println("form error: " + err.Error()) - http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable) - return - } - fo := r.MultipartForm - - body := bytes.Buffer{} - form := multipart.NewWriter(&body) - email := interfaces.Email{ - From: fo.Value["from"][0], - To: []string{}, - Cc: []string{}, - Subject: fo.Value["subject"][0], - Date: time.Now(), - } - email.ID = fmt.Sprintf("%s%s", strconv.FormatInt(email.Date.Unix(), 10), email.From) - - // headers - body.WriteString("MIME-Version: 1.0\r\n") - body.WriteString("From: " + email.From + "\r\n") - - for _, to := range fo.Value["to"] { - email.To = append(email.To, fmt.Sprintf("%s", to)) - } - body.WriteString("To: " + strings.Join(email.To, ", ") + "\r\n") - - if len(fo.Value["cc"]) > 0 && fo.Value["cc"][0] != "" { - for _, cc := range fo.Value["cc"] { - email.Cc = append(email.Cc, fmt.Sprintf("%s", cc)) - } - body.WriteString("Cc: " + strings.Join(email.Cc, ", ") + "\r\n") - } - - body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", email.ID)) - - body.WriteString("Date: " + email.Date.Format(time.RFC1123Z) + "\r\n") - body.WriteString("Subject: " + email.Subject + "\r\n") - body.WriteString("Content-Type: multipart/mixed; boundary=" + form.Boundary() + "\r\n\r\n") - - text, err := form.CreatePart( - map[string][]string{ - "Content-Type": {"text/plain; charset=\"UTF-8\""}, - }, - ) - if err != nil { - println("creatPart error: " + err.Error()) - http.Error(w, "creatPart error"+err.Error(), http.StatusInternalServerError) - return - } - text.Write([]byte(strings.Join(fo.Value["body"], ""))) - - for name, fi := range fo.File { - part, err := form.CreateFormFile(name, name) - if err != nil { - println("error creating form file: " + err.Error()) - continue - } - attach, err := fi[0].Open() - if err != nil { - println("error getting attachment: " + err.Error()) - continue - } - defer attach.Close() - - content, err := io.ReadAll(attach) - if err != nil { - println("error getting attachment: " + err.Error()) - continue - } - part.Write(content) - } - form.Close() - - // dkim - payload := bytes.Buffer{} - options := &dkim.SignOptions{ - Domain: "mail.blmayer.dev", - Selector: "dkim", - Signer: b.PrivateKey, - } - if err := dkim.Sign(&payload, &body, options); err != nil { - println("failed to sign body:", err.Error()) - } - email.Body = payload.String() - email.Raw = []byte(email.Body) - - // dns mx for email - for _, to := range email.To { - addr := strings.Split(to, "@") - mxs, err := net.LookupMX(addr[1]) - if err != nil { - println("get MX error: " + err.Error()) - http.Error(w, "get MX error"+err.Error(), http.StatusInternalServerError) - return - } - if len(mxs) == 0 { - println("empty MX response") - http.Error(w, "empty MX response", http.StatusInternalServerError) - return - } - - server := mxs[0].Host + ":smtp" - err = smtp.SendMail( - server, - nil, - email.From, - []string{to}, - bytes.NewReader(email.Raw), - ) - if err != nil { - println("sendMail error: " + err.Error()) - http.Error(w, "error sending email: "+err.Error(), http.StatusInternalServerError) - return - } - - saveFunction(email) - } - http.Redirect(w, r, "/", http.StatusFound) - } -}
commit e737530dfcc9a336d95519924f6a13f301720e10 Author: blmayer <bleemayer@gmail.com> Date: Tue Apr 18 19:15:34 2023 -0300 Improved sending email diff --git a/interfaces/file/file.go b/interfaces/file/file.go index d76463e..d1201c3 100644 --- a/interfaces/file/file.go +++ b/interfaces/file/file.go @@ -41,13 +41,14 @@ import ( type FileHandler struct { templates *template.Template root string - password string + assetsPath string domain string privateKey crypto.Signer + vault interfaces.Vault } -func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error) { - f := FileHandler{root: c.Root, password: c.Password, domain: c.Domain} +func NewFileHandler(c config.InboxConfig, v interfaces.Vault, fs map[string]any) (FileHandler, error) { + f := FileHandler{root: c.Root, vault: v, domain: c.Domain} if fs == nil { fs = map[string]any{} } @@ -57,6 +58,7 @@ func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error var err error if c.Templates != "" { + f.assetsPath = c.Templates f.templates, err = template.New(c.Root).Funcs(fs). ParseGlob(c.Templates + "/*.html") } @@ -84,7 +86,7 @@ func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error func (f FileHandler) IndexHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, pass, _ := r.BasicAuth() - if user != "x" || pass != f.password { + if user == "" || !f.vault.Validate(user, pass) { w.Header().Add("WWW-Authenticate", "Basic") http.Error(w, "wrong auth", http.StatusUnauthorized) return @@ -104,17 +106,66 @@ func (f FileHandler) IndexHandler() http.HandlerFunc { } } +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) SendHandler(b backend.Backend) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { println("file handling send email") user, pass, _ := r.BasicAuth() - if user != "x" || pass != f.password { + if user == "" || !f.vault.Validate(user, pass) { w.Header().Add("WWW-Authenticate", "Basic") http.Error(w, "wrong auth", http.StatusUnauthorized) return } - b.SendHandler(f.SaveSent)(w, r) + if err := r.ParseMultipartForm(20 * 1024 * 1024); err != nil { + println("form error: " + err.Error()) + http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable) + return + } + fo := r.MultipartForm + + email := interfaces.Email{ + From: fo.Value["from"][0], + To: fo.Value["to"], + Cc: fo.Value["cc"], + Subject: fo.Value["subject"][0], + Date: time.Now(), + Body: fo.Value["body"][0], + Attachments: map[string]interfaces.Attachment{}, + } + + for name, fi := range fo.File { + attach, err := fi[0].Open() + if err != nil { + println("error getting attachment: " + err.Error()) + continue + } + defer attach.Close() + + content, err := io.ReadAll(attach) + if err != nil { + println("error getting attachment: " + err.Error()) + continue + } + email.Attachments[name] = interfaces.Attachment{ + Name: name, + Data: content, + // TODO: Add content-type + } + } + + err := f.Send(email) + if err != nil { + println("send error: " + err.Error()) + http.Error(w, "send error"+err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/", http.StatusFound) } } @@ -209,7 +260,7 @@ func (f FileHandler) Send(mail interfaces.Email) error { if err := dkim.Sign(&payload, &body, options); err != nil { println("failed to sign body:", err.Error()) } - mail.Body = payload.String() + mail.Raw = payload.Bytes() // dns mx for email for _, to := range mail.To { @@ -228,7 +279,7 @@ func (f FileHandler) Send(mail interfaces.Email) error { nil, mail.From, []string{to}, - []byte(mail.Body), + mail.Raw, ) if err != nil { return err