This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Thu Apr 13 20:07:31 2023 -0300
Parent: 84634ca
Cleanup interfaces - Added support for per handler DKIM
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go index 3c1b238..b7d2cb4 100644 --- a/cmd/dovel/main.go +++ b/cmd/dovel/main.go @@ -1,11 +1,7 @@ package main import ( - "crypto" - "crypto/x509" "encoding/json" - "encoding/pem" - "io/ioutil" "log" "net/http" "os" @@ -27,10 +23,10 @@ var ( Server: config.ServerConfig{ Domain: "dovel.email", Address: ":2525", - DKIMKeyPath: "dkim.priv", Inboxes: []config.InboxConfig{ { Domain: "localhost", + DKIMKeyPath: "dkim.priv", Templates: "www", Handler: "file", Root: "mail", @@ -52,38 +48,18 @@ func main() { json.NewDecoder(configFile).Decode(&cfg) } - // load kdim key - if cfg.Server.DKIMKeyPath != "" { - key, err := ioutil.ReadFile(cfg.Server.DKIMKeyPath) - if err != nil { - panic(err) - } - - block, _ := pem.Decode(key) - if block == nil { - panic("no PEM data found") - } - privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - panic(err) - } - b.PrivateKey = privKey.(crypto.Signer) - println("loaded dkim private key") - } - for _, hand := range cfg.Server.Inboxes { switch hand.Handler { case "gwi": - g := gwi.GWIConfig{Root: hand.Root} + g, err := gwi.NewGWIHandler(hand) + if err != nil { + panic(err) + } + b.Handlers[hand.Domain] = g.GwiEmailHandler case "file": - mailCfg := file.FileConfig{ - Root: hand.Root, - Templates: &hand.Templates, - Password: os.Getenv("DOVEL_PASS"), - } funcs := map[string]any{"heading": heading} - mail, err := file.NewFileHandler(mailCfg, funcs) + mail, err := file.NewFileHandler(hand, funcs) if err != nil { panic(err) }
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/config/config.go b/config/config.go index af7435d..f3c21ab 100644 --- a/config/config.go +++ b/config/config.go @@ -20,10 +20,12 @@ type Config struct { } type InboxConfig struct { - Domain string - Templates string - Handler string - Root string + Domain string + DKIMKeyPath string + Templates string + Handler string + Root string + Password string } // ServerConfig is used to configure your email server. @@ -35,8 +37,7 @@ type InboxConfig struct { // 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 - DKIMKeyPath string + Address string + Domain string + Inboxes []InboxConfig }
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/index.html b/index.html index 3c768c9..60bd046 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ body { pre { font-weight: bold; - background: darkgray; + background: lightgray; padding: 8px; border-radius: 8px; } @@ -77,8 +77,18 @@ tt { <h3>Roadmap</h3> As aforementioned Dovel is in active development, this means there is - a lot to be done. The planned features and development progress can be - found on our <a href=//blmayer.dev/x/dovel/files/TODO.txt>TODO</a>. + a lot to be done. The major features planned are as follows: + <ul> + <li><s>Support DKIM.</s></li> + <li><s>Multiple inboxes.</s></li> + <li>PGP support.</li> + <li>Multiple users.</li> + <li>Support moving and deleting email on web.</li> + <li>Implement mailing lists behaviour.</li> + </ul> + + The detailed development features and progress can be found on our + <a href=//blmayer.dev/x/dovel/files/TODO.txt>TODO</a> file. <h2>Installation</h2> To install <tt>dovel</tt> you need <tt>golang</tt> propperly installed, @@ -118,12 +128,12 @@ tt { "server": { "address": ":25", "domain": "dovel.email", - "dKIMKeyPath": "dkim.priv", "inboxes": [ { "domain": "user.dovel.email", "handler": "file", "root": "mail/user", + "dKIMKeyPath": "dkim.priv", "templates": "www" } ] @@ -136,8 +146,10 @@ tt { addressed to <i>joe@user.dovel.email</i> with subject <i>test</i> will be saved in the <tt>mail/user/joe/test/</tt> folder. - <p>To see all options please refer to the - <a href=//pkg.go.dev/blmayer.dev/x/dovel> go docs</a>.</p> + <p> + To see all options please refer to the + <a href=//pkg.go.dev/blmayer.dev/x/dovel> go docs</a>. + </p> <h2>Using</h2> Using <tt>dovel</tt> email server and the web interface is meant to be @@ -147,13 +159,15 @@ tt { "x", the password is set using an environment variable. <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</tt> application to apply changes. + 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</tt> + application to apply changes. </p> <p> - I currently use dovel for my personal email and my git mail workflow. + I currently use dovel for my personal email and my git mail + workflow. </p> <h3>Functions</h3> @@ -167,9 +181,9 @@ tt { on <a href=//blmayer.dev/x>my projects page</a>. <p> - Financial support is also greatly appreciated, you can donate any - amount by using this <a href=//ko-fi.com/blmayer>ko-fi</a> link. - Thank you! + Financial support is also greatly appreciated, you can donate + any amount by using this <a href=//ko-fi.com/blmayer>ko-fi</a> + link. Thank you! </p> </body> </html>
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/interfaces/file/file.go b/interfaces/file/file.go index 0f3579f..d76463e 100644 --- a/interfaces/file/file.go +++ b/interfaces/file/file.go @@ -4,43 +4,49 @@ package file import ( + "bytes" + "crypto" + "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "html/template" "io" + "io/ioutil" + "mime/multipart" + "net" "net/http" + "net/smtp" "os" "path" "sort" + "strconv" + "strings" + "time" + "blmayer.dev/x/dovel/config" "blmayer.dev/x/dovel/interfaces" "blmayer.dev/x/dovel/interfaces/backend" "github.com/OfimaticSRL/parsemail" + "github.com/emersion/go-msgauth/dkim" ) -// FileConfig is used to configure the file handler, now it only contains -// the root folder that holds all emails. +// FileHandler is used to configure the file email handler. // Root is where mail should be saved, Templates is an optional field that // is the path to templates, to be used in the web server; Password is the // password for the x user, for now this is very simple; Domain is used to // filter and separate emails, only emails sent to one of your domains are // saved, each according to its configuration. -type FileConfig struct { - Root string - Templates *string - Password string - Domain string -} - type FileHandler struct { - templates *template.Template - root string - password string - domain string + templates *template.Template + root string + password string + domain string + privateKey crypto.Signer } -func NewFileHandler(c FileConfig, fs map[string]any) (FileHandler, error) { +func NewFileHandler(c config.InboxConfig, fs map[string]any) (FileHandler, error) { f := FileHandler{root: c.Root, password: c.Password, domain: c.Domain} if fs == nil { fs = map[string]any{} @@ -50,10 +56,28 @@ func NewFileHandler(c FileConfig, fs map[string]any) (FileHandler, error) { fs["mail"] = f.Mail var err error - if c.Templates != nil { + if c.Templates != "" { f.templates, err = template.New(c.Root).Funcs(fs). - ParseGlob(*c.Templates + "/*.html") + ParseGlob(c.Templates + "/*.html") } + + if c.DKIMKeyPath != "" { + key, err := ioutil.ReadFile(c.DKIMKeyPath) + if err != nil { + return f, err + } + + block, _ := pem.Decode(key) + if block == nil { + return f, err + } + privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return f, err + } + f.privateKey = privKey.(crypto.Signer) + } + return f, err } @@ -134,6 +158,86 @@ func (f FileHandler) SaveSent(email interfaces.Email) error { return nil } +func (f FileHandler) Send(mail interfaces.Email) error { + mail.ID = fmt.Sprintf("%s%s", strconv.FormatInt(mail.Date.Unix(), 10), mail.From) + + body := bytes.Buffer{} + form := multipart.NewWriter(&body) + + // headers + body.WriteString("MIME-Version: 1.0\r\n") + body.WriteString("From: " + mail.From + "\r\n") + body.WriteString("To: " + strings.Join(mail.To, ", ") + "\r\n") + if len(mail.Cc) > 0 { + body.WriteString("Cc: " + strings.Join(mail.Cc, ", ") + "\r\n") + } + + body.WriteString(fmt.Sprintf("Message-ID: <%s>\r\n", mail.ID)) + + body.WriteString("Date: " + mail.Date.Format(time.RFC1123Z) + "\r\n") + body.WriteString("Subject: " + mail.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 { + return err + } + text.Write([]byte(mail.Body)) + + for name, fi := range mail.Attachments { + part, err := form.CreateFormFile(name, name) + if err != nil { + println("error creating form file: " + err.Error()) + continue + } + + part.Write(fi.Data) + } + form.Close() + + // dkim + payload := bytes.Buffer{} + options := &dkim.SignOptions{ + Domain: f.domain, + Selector: "dkim", + Signer: f.privateKey, + } + if err := dkim.Sign(&payload, &body, options); err != nil { + println("failed to sign body:", err.Error()) + } + mail.Body = payload.String() + + // dns mx for email + for _, to := range mail.To { + addr := strings.Split(to, "@") + mxs, err := net.LookupMX(addr[1]) + if err != nil { + return err + } + if len(mxs) == 0 { + return err + } + + server := mxs[0].Host + ":smtp" + err = smtp.SendMail( + server, + nil, + mail.From, + []string{to}, + []byte(mail.Body), + ) + if err != nil { + return err + } + + } + return f.SaveSent(mail) +} + func (f FileHandler) Mailboxes(folder string) ([]interfaces.Mailbox, error) { fmt.Println("mailer mailboxes for", folder) dir, err := os.ReadDir(path.Join(f.root, folder)) @@ -236,10 +340,11 @@ func (f FileHandler) Mail(file string) (interfaces.Email, error) { continue } + encContent := base64.StdEncoding.EncodeToString(content) email.Attachments[a.Filename] = interfaces.Attachment{ Name: a.Filename, ContentType: a.ContentType, - Data: base64.StdEncoding.EncodeToString(content), + Data: []byte(encContent), } } return email, nil
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go index cc98d1d..30dd74e 100644 --- a/interfaces/gwi/gwi.go +++ b/interfaces/gwi/gwi.go @@ -7,6 +7,7 @@ import ( "path" "strings" + "blmayer.dev/x/dovel/config" "blmayer.dev/x/dovel/interfaces" ) @@ -15,14 +16,18 @@ import ( // Commands lets you add functions that will run for each received email, the // key of the map specifies a trigger in the form "key!". That is, if the email // body starts with key! then the command key is run. -type GWIConfig struct { +type GWIHandler struct { Root string Commands map[string]func(email interfaces.Email) error } +func NewGWIHandler(c config.InboxConfig) (GWIHandler, error) { + return GWIHandler{Root: c.Root}, nil +} + // GwiEmailHandler saves emails to the correct repo using the subject to // separate them. Subject fiel must be of form "[%s] %s". -func (g GWIConfig) GwiEmailHandler(email interfaces.Email) error { +func (g GWIHandler) GwiEmailHandler(email interfaces.Email) error { userRepoDomain := strings.Split(email.To[0], "@") userRepo := strings.Split(userRepoDomain[0], "/") if len(userRepo) != 2 {
commit 937261b764e8c9e028be77fe6a079330b65da690 Author: blmayer <bleemayer@gmail.com> Date: Thu Apr 13 20:07:31 2023 -0300 Cleanup interfaces - Added support for per handler DKIM diff --git a/interfaces/main.go b/interfaces/main.go index 5f56421..eb5ccf8 100644 --- a/interfaces/main.go +++ b/interfaces/main.go @@ -40,7 +40,7 @@ type Mailbox struct { type Attachment struct { Name string ContentType string - Data string + Data []byte } type Email struct { @@ -74,3 +74,9 @@ func ToEmail(mail parsemail.Email) Email { } return m } + +type Mailer interface { + Send(mail Email) error + Save(mail Email) error +} +