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

35f070d

Author: bmayer3 (bmayer@sibros.tech)

Date: Thu Feb 9 13:11:25 2023 -0300

Parent: cb90b94

Organised backend struct

Diff

cmd/dovel/main.go

commit 35f070d3526e8257de68b80cbfb02065a25de692
Author: bmayer3 <bmayer@sibros.tech>
Date:   Thu Feb 9 13:11:25 2023 -0300

    Organised backend struct

diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index 7ec4893..7a2d434 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -2,12 +2,9 @@ package main
 
 import (
 	"encoding/json"
-	"fmt"
-	"io"
 	"log"
 	"net/http"
 	"os"
-	"strings"
 	"time"
 
 	"blmayer.dev/x/dovel/config"
@@ -16,86 +13,8 @@ import (
 	"blmayer.dev/x/dovel/interfaces/gwi"
 
 	"github.com/emersion/go-smtp"
-
-	"github.com/marcospgmelo/parsemail"
 )
 
-type backend struct {
-	handlers map[string]config.Handler
-}
-
-func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
-	return Session{handlers: b.handlers}, nil
-}
-
-func (b backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
-	return Session{}, nil
-}
-
-func (b backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
-	return Session{handlers: b.handlers}, nil
-}
-
-// A Session is returned after EHLO.
-type Session struct {
-	User     string
-	handlers map[string]config.Handler
-}
-
-func (s Session) AuthPlain(username, password string) error {
-	println("connection sent", username, password)
-	return nil
-}
-
-func (s Session) Mail(from string, opts smtp.MailOptions) error {
-	println("Mail from:", from)
-	return nil
-}
-
-func (s Session) Rcpt(to string) error {
-	println("Rcpt to:", to)
-	return nil
-}
-
-func (s Session) Data(r io.Reader) error {
-	content, err := io.ReadAll(r)
-	if err != nil {
-		println("read content", err.Error())
-		return err
-	}
-
-	email, err := parsemail.Parse(strings.NewReader(string(content)))
-	if err != nil {
-		println("parse email", err.Error())
-		return err
-	}
-
-	// get user from to field
-	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)
-		}
-		mail.To = []string{to}
-		if err := handler(mail, content); err != nil {
-			println("handler error", err.Error())
-			return fmt.Errorf("handler error %s", err.Error())
-		}
-	}
-
-	return nil
-}
-
-func (s Session) Reset() {}
-
-func (s Session) Logout() error {
-	println("logged out")
-	return nil
-}
-
 var (
 	defaultPort   = "8080"
 	defaultConfig = config.Config{
@@ -113,6 +32,7 @@ var (
 			},
 		},
 	}
+	b = backend{handlers: map[string]config.Handler{}}
 )
 
 func main() {
@@ -126,7 +46,6 @@ func main() {
 		json.NewDecoder(configFile).Decode(&cfg)
 	}
 
-	b := backend{handlers: map[string]config.Handler{}}
 	for _, hand := range cfg.Server.Inboxes {
 		switch hand.Handler {
 		case "gwi":

cmd/dovel/server.go

commit 35f070d3526e8257de68b80cbfb02065a25de692
Author: bmayer3 <bmayer@sibros.tech>
Date:   Thu Feb 9 13:11:25 2023 -0300

    Organised backend struct

diff --git a/cmd/dovel/server.go b/cmd/dovel/server.go
new file mode 100644
index 0000000..bc8a772
--- /dev/null
+++ b/cmd/dovel/server.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"blmayer.dev/x/dovel/config"
+	"blmayer.dev/x/dovel/interfaces"
+	"github.com/emersion/go-smtp"
+	"github.com/marcospgmelo/parsemail"
+)
+
+// A Session is returned after EHLO.
+type Session struct {
+	User     string
+	handlers map[string]config.Handler
+}
+
+func (s Session) AuthPlain(username, password string) error {
+	println("connection sent", username, password)
+	return nil
+}
+
+func (s Session) Mail(from string, opts smtp.MailOptions) error {
+	println("Mail from:", from)
+	return nil
+}
+
+func (s Session) Rcpt(to string) error {
+	println("Rcpt to:", to)
+	return nil
+}
+
+func (s Session) Data(r io.Reader) error {
+	content, err := io.ReadAll(r)
+	if err != nil {
+		println("read content", err.Error())
+		return err
+	}
+
+	email, err := parsemail.Parse(strings.NewReader(string(content)))
+	if err != nil {
+		println("parse email", err.Error())
+		return err
+	}
+
+	// get user from to field
+	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)
+		}
+		mail.To = []string{to}
+		if err := handler(mail, content); err != nil {
+			println("handler error", err.Error())
+			return fmt.Errorf("handler error %s", err.Error())
+		}
+	}
+
+	return nil
+}
+
+func (s Session) Reset() {}
+
+func (s Session) Logout() error {
+	println("logged out")
+	return nil
+}

config/config.go

commit 35f070d3526e8257de68b80cbfb02065a25de692
Author: bmayer3 <bmayer@sibros.tech>
Date:   Thu Feb 9 13:11:25 2023 -0300

    Organised backend struct

diff --git a/config/config.go b/config/config.go
index 0bf7a34..6ec40d7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -31,8 +31,11 @@ type InboxConfig struct {
 // 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
+	Address     string
+	Domain      string
+	Inboxes     []InboxConfig
+	DKIMPrivKey string
+	DKIMPubKey  string
 }

interfaces/backend/backend.go

commit 35f070d3526e8257de68b80cbfb02065a25de692
Author: bmayer3 <bmayer@sibros.tech>
Date:   Thu Feb 9 13:11:25 2023 -0300

    Organised backend struct

diff --git a/interfaces/backend/backend.go b/interfaces/backend/backend.go
new file mode 100644
index 0000000..93dccb3
--- /dev/null
+++ b/interfaces/backend/backend.go
@@ -0,0 +1,154 @@
+// package backend 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 backend
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"blmayer.dev/x/dovel/config"
+	"blmayer.dev/x/dovel/interfaces"
+	"github.com/emersion/go-smtp"
+)
+
+type Backend struct {
+	handlers map[string]config.Handler
+}
+
+func (b Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
+	return Session{handlers: b.handlers}, nil
+}
+
+func (b Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
+	return Session{}, nil
+}
+
+func (b Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
+	return Session{handlers: b.handlers}, nil
+}
+
+func (b Backend) SendHandler(saveFunction func(e interfaces.Email, b []byte) error) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		user, pass, _ := r.BasicAuth()
+		if user != "x" || pass != f.password {
+			w.Header().Add("WWW-Authenticate", "Basic")
+			http.Error(w, "wrong auth", http.StatusUnauthorized)
+			return
+		}
+
+		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()
+
+		// 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},
+				body.Bytes(),
+			)
+			if err != nil {
+				println("sendMail error: " + err.Error())
+				http.Error(w, "error sending email: "+err.Error(), http.StatusInternalServerError)
+				return
+			}
+
+			saveFunction(email, body.Bytes())
+		}
+		http.Redirect(w, r, "/", http.StatusFound)
+	}
+}

interfaces/file/file.go

commit 35f070d3526e8257de68b80cbfb02065a25de692
Author: bmayer3 <bmayer@sibros.tech>
Date:   Thu Feb 9 13:11:25 2023 -0300

    Organised backend struct

diff --git a/interfaces/file/file.go b/interfaces/file/file.go
index afdb2e6..a68a596 100644
--- a/interfaces/file/file.go
+++ b/interfaces/file/file.go
@@ -1,23 +1,17 @@
 package file
 
 import (
-	"bytes"
 	"encoding/base64"
 	"fmt"
 	"html/template"
 	"io"
-	"mime/multipart"
-	"net"
 	"net/http"
-	"net/smtp"
 	"os"
 	"path"
 	"sort"
-	"strconv"
-	"strings"
-	"time"
 
 	"blmayer.dev/x/dovel/interfaces"
+	"blmayer.dev/x/dovel/interfaces/backend"
 
 	"github.com/marcospgmelo/parsemail"
 )
@@ -83,122 +77,8 @@ func (f FileHandler) IndexHandler() http.HandlerFunc {
 	}
 }
 
-func (f FileHandler) SendHandler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		user, pass, _ := r.BasicAuth()
-		if user != "x" || pass != f.password {
-			w.Header().Add("WWW-Authenticate", "Basic")
-			http.Error(w, "wrong auth", http.StatusUnauthorized)
-			return
-		}
-
-		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()
-
-		// 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},
-				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) SendHandler(b backend.Backend) http.HandlerFunc {
+	return b.SendHandler(f.SaveSent)
 }
 
 func (f FileHandler) Save(email interfaces.Email, content []byte) error {