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