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

937261b

Author: blmayer (bleemayer@gmail.com)

Date: Thu Apr 13 20:07:31 2023 -0300

Parent: 84634ca

Cleanup interfaces

- Added support for per handler DKIM

Diff

cmd/dovel/main.go

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)
 			}

config/config.go

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
 }

index.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/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>

interfaces/file/file.go

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

interfaces/gwi/gwi.go

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 {

interfaces/main.go

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
+}
+