list

server

This is the main dovel repository, it has the Go code to run dovel SMTP server.

curl https://dovel.email/server.tar tar

8df1fb0

Author: blmayer (bleemayer@gmail.com)

Date: Sat Jan 21 22:49:40 2023 -0300

Parent: 7444744

Fixed sending email

Diff

Makefile

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/Makefile b/Makefile
index 3f70abd..7d7b6cc 100644
--- a/Makefile
+++ b/Makefile
@@ -9,3 +9,6 @@ deploy: dovel
 
 deploy-site: index.html
 	scp $^ zero:dovel.email/
+
+deploy-pages: $(wildcard www/*)
+	scp $^ zero:blmayer.dev/www/mail

cmd/dovel/main.go

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index dc74fd5..e1fa6c2 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"blmayer.dev/x/dovel/config"
+	"blmayer.dev/x/dovel/interfaces"
 	"blmayer.dev/x/dovel/interfaces/file"
 	"blmayer.dev/x/dovel/interfaces/gwi"
 	"github.com/emersion/go-smtp"
@@ -68,14 +69,16 @@ func (s Session) Data(r io.Reader) error {
 	}
 
 	// get user from to field
-	for _, to := range email.To {
-		userDomain := strings.Split(to.Address, "@")
+	mail := interfaces.ToEmail(email)
+	for _, to := range mail.To {
+		userDomain := strings.Split(to, "@")
 		handler, ok := s.handlers[userDomain[1]]
 		if !ok {
 			println("no handler for domain", userDomain[1])
 			return fmt.Errorf("no handler for domain %s", userDomain)
 		}
-		if err := handler(email, content); err != nil {
+		mail.To = []string{to}
+		if err := handler(mail, content); err != nil {
 			println("handler error", err.Error())
 			return fmt.Errorf("handler error %s", err.Error())
 		}
@@ -126,7 +129,7 @@ func main() {
 		switch hand.Handler {
 		case "gwi":
 			g := gwi.GWIConfig{Root: hand.Root}
-			g.Commands = map[string]func(email parsemail.Email, thread string) error{
+			g.Commands = map[string]func(email interfaces.Email, thread string) error{
 				"close": g.Close,
 			}
 			b.handlers[hand.Domain] = g.GwiEmailHandler

config/config.go

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/config/config.go b/config/config.go
index 3b7eb8b..191b27f 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,10 +1,10 @@
 package config
 
 import (
-	"github.com/vraycc/go-parsemail"
+	"blmayer.dev/x/dovel/interfaces"
 )
 
-type Handler func(email parsemail.Email, content []byte) error
+type Handler func(email interfaces.Email, content []byte) error
 
 // Config is the configuration structure used to set up dovel
 // server and web interface. This should be a JSON file located

index.html

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/index.html b/index.html
index 6978c2a..ad434c4 100644
--- a/index.html
+++ b/index.html
@@ -140,7 +140,7 @@ tt {
 	<p>
 	As a starting point, look at the <tt>www/</tt> folder in our git repo.
 	They are templates that you can customize to your needs. Once you're
-	done simply restart the <tt>dovel-web</tt> application to apply
+	done simply restart the <tt>dovel</tt> application to apply
 	changes.
 	</p>
 

interfaces/file/file.go

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
index d5a0300..731c4ed 100644
--- a/interfaces/file/file.go
+++ b/interfaces/file/file.go
@@ -13,6 +13,7 @@ import (
 	"os"
 	"path"
 	"strings"
+	"strconv"
 	"time"
 
 	"blmayer.dev/x/dovel/interfaces"
@@ -25,16 +26,18 @@ type FileConfig struct {
 	Root      string
 	Templates string
 	Password  string
+	Domain    string
 }
 
 type FileHandler struct {
 	templates *template.Template
 	root      string
 	password  string
+	domain string
 }
 
 func NewFileHandler(c FileConfig, fs map[string]any) (FileHandler, error) {
-	f := FileHandler{root: c.Root, password: c.Password}
+	f := FileHandler{root: c.Root, password: c.Password, domain: c.Domain}
 	fs["inboxes"] = f.Mailboxes
 	fs["mails"] = f.Mails
 	fs["mail"] = f.Mail
@@ -84,28 +87,55 @@ func (f FileHandler) SendHandler() http.HandlerFunc {
 			http.Error(w, "form error"+err.Error(), http.StatusNotAcceptable)
 			return
 		}
-		f := r.MultipartForm
+		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("From: " + strings.Join(f.Value["from"], ", ") + "\r\n")
-		body.WriteString("To: " + strings.Join(append(f.Value["to"], f.Value["cco"]...), ", ") + "\r\n")
-		body.WriteString("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n")
-		body.WriteString("Subject: " + f.Value["subject"][0] + "\r\n")
-		body.WriteString("Content-Type: multipart/mixed; boundary=" + form.Boundary() + "\r\n")
+		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")
+		}
 
-		text, err := form.CreatePart(map[string][]string{"Content-Type": []string{"text/plain"}})
+		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
+		 	println("creatPart error: " + err.Error())
+		 	http.Error(w, "creatPart error"+err.Error(), http.StatusInternalServerError)
+		 	return
 		}
-		text.Write([]byte(strings.Join(f.Value["body"], "")))
+		text.Write([]byte(strings.Join(fo.Value["body"], "")))
 
-		for _, fi := range f.File {
-			part, err := form.CreateFormFile(fi[0].Filename, fi[0].Filename)
+		for name, fi := range fo.File {
+			part, err := form.CreateFormFile(name, name)
 			if err != nil {
 				println("error creating form file: " + err.Error())
 				continue
@@ -124,54 +154,77 @@ func (f FileHandler) SendHandler() http.HandlerFunc {
 			}
 			part.Write(content)
 		}
-		email := body.String()
+		form.Close()
 
 		// dns mx for email
-		addr := strings.Split(f.Value["to"][0], "@")
-		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 reponse", http.StatusInternalServerError)
-			return
-		}
+		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 reponse", http.StatusInternalServerError)
+				return
+			}
 
-		server := mxs[0].Host + ":smtp"
-		println("sending email to " + server + ":\n" + email)
+			server := mxs[0].Host + ":smtp"
+			err = smtp.SendMail(
+				server,
+				nil,
+				email.From,
+				[]string{to},
+				body.Bytes(),
+			)
+			if err != nil {
+				println("sendMail error: " + err.Error())
+				http.Error(w, "error sending email: "+err.Error(), http.StatusInternalServerError)
+				return
+			}
 
-		err = smtp.SendMail(
-			server,
-			nil,
-			f.Value["from"][0],
-			append(f.Value["to"], f.Value["cc"]...),
-			body.Bytes(),
-		)
-		if err != nil {
-			println("sendMail error: " + err.Error())
-			http.Error(w, "error sending email: "+err.Error(), http.StatusInternalServerError)
-			return
+			f.SaveSent(email, body.Bytes())
 		}
 		http.Redirect(w, r, "/", http.StatusFound)
 	}
 }
 
-func (f FileHandler) Save(email parsemail.Email, content []byte) error {
-	to := strings.Split(email.To[0].Address, "@")[0]
-	mailDir := path.Join(f.root, to, email.Subject)
+func (f FileHandler) Save(email interfaces.Email, content []byte) error {
+	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
+	}
+	_, err = file.Write(content)
+	if err != nil {
+		println("file write", err.Error())
+		return err
+	}
+
+	return nil
+}
+
+func (f FileHandler) SaveSent(email interfaces.Email, content []byte) error {
+	mailDir := path.Join(f.root, email.From, email.Subject)
 	os.MkdirAll(mailDir, os.ModeDir|0o700)
 
-	file, err := os.Create(path.Join(mailDir, email.MessageID))
+	file, err := os.Create(path.Join(mailDir, email.ID))
 	if err != nil {
 		println("file create", err.Error())
 		return err
 	}
 	_, err = file.Write(content)
+	if err != nil {
+		println("file write", err.Error())
+		return err
+	}
 
-	return err
+	return nil
 }
 
 func (f FileHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) {
@@ -247,16 +300,20 @@ func (f FileHandler) Mail(file string) (interfaces.Email, error) {
 
 	email := interfaces.Email{
 		ID:          mail.MessageID,
-		To:          mail.To[0].Address,
 		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 {
-		email.Cc = mail.Cc[0].Address
+		for _, cc := range mail.Cc {
+			email.Cc = append(email.Cc, cc.Address)
+		}
 	}
 
 	for _, a := range mail.Attachments {

interfaces/gwi/gwi.go

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
index 9cc85f2..18545df 100644
--- a/interfaces/gwi/gwi.go
+++ b/interfaces/gwi/gwi.go
@@ -6,16 +6,16 @@ import (
 	"path"
 	"strings"
 
-	"github.com/vraycc/go-parsemail"
+	"blmayer.dev/x/dovel/interfaces"
 )
 
 type GWIConfig struct {
 	Root     string
-	Commands map[string]func(email parsemail.Email, thread string) error
+	Commands map[string]func(email interfaces.Email, thread string) error
 }
 
-func (g GWIConfig) GwiEmailHandler(email parsemail.Email, content []byte) error {
-	userRepoDomain := strings.Split(email.To[0].Address, "@")
+func (g GWIConfig) GwiEmailHandler(email interfaces.Email, content []byte) error {
+	userRepoDomain := strings.Split(email.To[0], "@")
 	userRepo := strings.Split(userRepoDomain[0], "/")
 	if len(userRepo) != 2 {
 		println("received bad to", userRepo)
@@ -46,7 +46,7 @@ func (g GWIConfig) GwiEmailHandler(email parsemail.Email, content []byte) error
 	mailDir = path.Join(g.Root, user, repo, "mail/open", title)
 	os.MkdirAll(mailDir, os.ModeDir|0o700)
 
-	mailFile, err := os.Create(path.Join(mailDir, email.MessageID))
+	mailFile, err := os.Create(path.Join(mailDir, email.ID))
 	if err != nil {
 		println("create mail file", err.Error())
 		return err
@@ -64,7 +64,7 @@ func (g GWIConfig) GwiEmailHandler(email parsemail.Email, content []byte) error
 		println("gwi applying commands")
 		for com, f := range g.Commands {
 			println("gwi applying", com)
-			if !strings.HasPrefix(email.TextBody, "!"+com) {
+			if !strings.HasPrefix(email.Body, "!"+com) {
 				continue
 			}
 			if err := f(email, mailDir); err != nil {
@@ -77,7 +77,7 @@ func (g GWIConfig) GwiEmailHandler(email parsemail.Email, content []byte) error
 	return err
 }
 
-func (g GWIConfig) Close(email parsemail.Email, threadPath string) error {
+func (g GWIConfig) Close(email interfaces.Email, threadPath string) error {
 	println("gwi closing thread", threadPath)
 
 	// threadPath is like "/.../git/user/repo/mail/open/thread

interfaces/main.go

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/interfaces/main.go b/interfaces/main.go
index d3b297d..8c7c4e1 100644
--- a/interfaces/main.go
+++ b/interfaces/main.go
@@ -7,6 +7,7 @@ import (
 	"time"
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/vraycc/go-parsemail"
 )
 
 type User interface {
@@ -43,10 +44,30 @@ type Attachment struct {
 type Email struct {
 	ID          string
 	From        string
-	To          string
-	Cc          string
+	To          []string
+	Cc          []string
+	BCc         []string
 	Date        time.Time
 	Subject     string
 	Body        string
 	Attachments map[string]Attachment
 }
+
+func ToEmail(mail parsemail.Email) Email {
+	m := Email{
+		From: mail.Sender.Address,
+		To: []string{},
+		Cc: []string{},
+		Subject: mail.Subject,
+		ID: mail.MessageID,
+		Date: mail.Date,
+		Body: mail.TextBody,
+	}
+	for _, to := range mail.To {
+		m.To = append(m.To, to.Address)
+	}
+	for _, cc := range mail.Cc {
+		m.Cc = append(m.Cc, cc.Address)
+	}
+	return m
+}

www/compose.html

commit 8df1fb0050ef84d8d8a11089b521b012b206a6ad
Author: blmayer <bleemayer@gmail.com>
Date:   Sat Jan 21 22:49:40 2023 -0300

    Fixed sending email

diff --git a/www/compose.html b/www/compose.html
index 3ca1c02..0605c73 100644
--- a/www/compose.html
+++ b/www/compose.html
@@ -6,7 +6,7 @@
 <pre>
 <b>// composing</b>
 <form action=/out method=post enctype=multipart/form-data>
-// From: <input name=from placeholder="___________" {{if .inbox}}value="{{index .inbox 0}}@m.blmayer.dev"{{end}}>
+// From: <input name=from placeholder="___________" {{if .inbox}}value="{{index .inbox 0}}"{{end}}>
 // To: <input name=to placeholder="________">
 // CC: <input name=cc placeholder="________">
 // CCo: <input name=cco placeholder="________">