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