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