This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: bmayer3 (bmayer@sibros.tech)
Date: Thu Feb 9 13:11:25 2023 -0300
Parent: cb90b94
Organised backend struct
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":
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 +}
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 }
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) + } +}
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 {