This is the main dovel repository, it has the Go code to run dovel SMTP server.
Author: blmayer (bleemayer@gmail.com)
Date: Mon Apr 17 20:57:54 2023 -0300
Parent: e0125f9
Made progress on user auth
commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date: Mon Apr 17 20:57:54 2023 -0300
Made progress on user auth
diff --git a/cmd/dovel/main.go b/cmd/dovel/main.go
index b7d2cb4..105d18f 100644
--- a/cmd/dovel/main.go
+++ b/cmd/dovel/main.go
@@ -21,15 +21,15 @@ var (
defaultConfig = config.Config{
WebPort: &defaultPort,
Server: config.ServerConfig{
- Domain: "dovel.email",
- Address: ":2525",
+ Domain: "dovel.email",
+ Address: ":2525",
Inboxes: []config.InboxConfig{
{
- Domain: "localhost",
+ Domain: "localhost",
DKIMKeyPath: "dkim.priv",
- Templates: "www",
- Handler: "file",
- Root: "mail",
+ Templates: "www",
+ Handler: "file",
+ Root: "mail",
},
},
},
@@ -51,12 +51,17 @@ func main() {
for _, hand := range cfg.Server.Inboxes {
switch hand.Handler {
case "gwi":
- g, err := gwi.NewGWIHandler(hand)
+ // load gwi user file
+ v, err := NewPGPVault(path.Join(hand.Root, "users.json"))
+ if err != nil {
+ panic(err)
+ }
+ g, err := gwi.NewGWIHandler(hand, v)
if err != nil {
panic(err)
}
- b.Handlers[hand.Domain] = g.GwiEmailHandler
+ b.Handlers[hand.Domain] = g.Save
case "file":
funcs := map[string]any{"heading": heading}
mail, err := file.NewFileHandler(hand, funcs)
@@ -106,5 +111,3 @@ func newAssetsHandler(root string) http.HandlerFunc {
http.ServeFile(w, r, path.Join(root, r.URL.Path[1:]))
}
}
-
-
commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date: Mon Apr 17 20:57:54 2023 -0300
Made progress on user auth
diff --git a/cmd/dovel/vault.go b/cmd/dovel/vault.go
new file mode 100644
index 0000000..1cc6d12
--- /dev/null
+++ b/cmd/dovel/vault.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "encoding/json"
+ "os"
+
+ "blmayer.dev/x/dovel/interfaces"
+)
+
+type pgpUser struct {
+ Name string
+ PGP string
+ Address string
+}
+
+func (u pgpUser) Email() string {
+ return u.Address
+}
+
+func (u pgpUser) Login() string {
+ return u.Name
+}
+
+func (u pgpUser) Pass() string {
+ return u.PGP
+}
+
+type pgpVault struct {
+ Users []pgpUser
+}
+
+func NewPGPVault(path string) (interfaces.Vault, error) {
+ s := pgpVault{Users: []pgpUser{}}
+
+ file, err := os.Open(path)
+ if err != nil {
+ return s, err
+ }
+ defer file.Close()
+
+ err = json.NewDecoder(file).Decode(&s.Users)
+ if err != nil {
+ return s, err
+ }
+
+ return s, nil
+}
+
+func (f pgpVault) GetUser(login string) interfaces.User {
+ for _, u := range f.Users {
+ if u.Name == login {
+ return u
+ }
+ }
+ return nil
+}
+
+// Validate is not used here. Only here to fill the interface
+func (f pgpVault) Validate(login, pass string) bool {
+ return false
+}
commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date: Mon Apr 17 20:57:54 2023 -0300
Made progress on user auth
diff --git a/interfaces/gwi/gwi.go b/interfaces/gwi/gwi.go
index 30dd74e..d681671 100644
--- a/interfaces/gwi/gwi.go
+++ b/interfaces/gwi/gwi.go
@@ -2,13 +2,24 @@
package gwi
import (
+ "bytes"
+ "crypto"
+ "crypto/x509"
+ "encoding/pem"
"fmt"
+ "io/ioutil"
+ "mime/multipart"
+ "net"
+ "net/smtp"
"os"
"path"
+ "strconv"
"strings"
+ "time"
"blmayer.dev/x/dovel/config"
"blmayer.dev/x/dovel/interfaces"
+ "github.com/emersion/go-msgauth/dkim"
)
// GWIConfig is used to configure the GWI interface for dovel. Root is the
@@ -17,17 +28,37 @@ import (
// 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 GWIHandler struct {
- Root string
- Commands map[string]func(email interfaces.Email) error
+ Root string
+ Commands map[string]func(email interfaces.Email) error
+ domain string
+ privateKey crypto.Signer
+ vault interfaces.Vault
}
-func NewGWIHandler(c config.InboxConfig) (GWIHandler, error) {
- return GWIHandler{Root: c.Root}, nil
+func NewGWIHandler(c config.InboxConfig, vault interfaces.Vault) (GWIHandler, error) {
+ g := GWIHandler{Root: c.Root, domain: c.Domain, vault: vault}
+ if c.DKIMKeyPath != "" {
+ key, err := ioutil.ReadFile(c.DKIMKeyPath)
+ if err != nil {
+ return g, err
+ }
+
+ block, _ := pem.Decode(key)
+ if block == nil {
+ return g, err
+ }
+ privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ return g, err
+ }
+ g.privateKey = privKey.(crypto.Signer)
+ }
+ return g, nil
}
-// GwiEmailHandler saves emails to the correct repo using the subject to
-// separate them. Subject fiel must be of form "[%s] %s".
-func (g GWIHandler) GwiEmailHandler(email interfaces.Email) error {
+// Save saves emails to the correct repo using the subject to
+// separate them. Subject field must be of form "[%s] %s".
+func (g GWIHandler) Save(email interfaces.Email) error {
userRepoDomain := strings.Split(email.To[0], "@")
userRepo := strings.Split(userRepoDomain[0], "/")
if len(userRepo) != 2 {
@@ -66,19 +97,120 @@ func (g GWIHandler) GwiEmailHandler(email interfaces.Email) error {
}
// apply commands
- go func() {
- println("gwi applying commands")
- for com, f := range g.Commands {
- println("gwi applying", com)
- if !strings.HasPrefix(email.Body, "!"+com) {
- continue
- }
- if err := f(email); err != nil {
- println(com, "error", err.Error())
- continue
- }
- println("gwi", com, "applied")
+ //go func() {
+ // println("gwi applying commands")
+ // for com, f := range g.Commands {
+ // println("gwi applying", com)
+ // if !strings.HasPrefix(email.Body, "!"+com) {
+ // continue
+ // }
+ // if err := f(email); err != nil {
+ // println(com, "error", err.Error())
+ // continue
+ // }
+ // println("gwi", com, "applied")
+ // }
+ //}()
+
+ // notify owner
+ owner := g.vault.GetUser(user)
+ if owner == nil || owner.Email() == "" {
+ return nil
+ }
+ email.To = []string{owner.Email()}
+ email.Body = fmt.Sprintf(
+ `You received an email with the subject %s.
+
+ Check you project by visiting https://%s/%s/%s
+
+ Yours.
+
+ The GWI team.`,
+ email.Subject,
+ g.domain,
+ user,
+ repo,
+ )
+ email.Subject = "New mail on project " + repo
+ email.From = fmt.Sprintf("%s/%s@%s", user, repo, g.domain)
+ return g.Send(email)
+}
+
+func (g GWIHandler) 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: g.domain,
+ Selector: "dkim",
+ Signer: g.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
}
- }()
- 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 nil
}
commit 2abc1253b185c3b0ef3af751eb8cd7907b092531
Author: blmayer <bleemayer@gmail.com>
Date: Mon Apr 17 20:57:54 2023 -0300
Made progress on user auth
diff --git a/interfaces/main.go b/interfaces/main.go
index eb5ccf8..09300a2 100644
--- a/interfaces/main.go
+++ b/interfaces/main.go
@@ -18,6 +18,11 @@ type User interface {
Pass() string
}
+type Vault interface {
+ GetUser(login string) User
+ Validate(login, pass string) bool
+}
+
type File struct {
*object.File
Size int64