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 {