This is the main dovel repository, it has the Go code to run dovel SMTP server.
- package main
- import (
- "bytes"
- "fmt"
- "io"
- "log/slog"
- "net"
- "net/mail"
- "os"
- "os/exec"
- "path"
- "strings"
- "time"
- "github.com/emersion/go-mbox"
- "github.com/emersion/go-smtp"
- )
- // A Session is returned after EHLO.
- type Session struct {
- user User
- from string
- tos []string
- }
- func (s *Session) AuthPlain(username, password string) error {
- slog.Debug("authenticating", "user", username, "pass", password)
- if !v.Validate(username, password) {
- slog.Warn("unauthorized", "user", username)
- return fmt.Errorf("user not authorized")
- }
- slog.Debug("authorized", "user", username)
- s.user = v.GetUser(username)
- return nil
- }
- func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
- slog.Debug("received mail", "from", from)
- s.from = from
- return nil
- }
- func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
- slog.Debug("received rcpt to", "to", to)
- s.tos = append(s.tos, to)
- return nil
- }
- func (s *Session) Data(raw io.Reader) error {
- cont, err := io.ReadAll(raw)
- if err != nil {
- return err
- }
- slog.Debug("received data", "data", cont)
- w := bytes.Buffer{}
- box := mbox.NewWriter(&w)
- boxW, err := box.CreateMessage(s.from, time.Now())
- if err != nil {
- return err
- }
- boxW.Write(cont)
- box.Close()
- mess, err := io.ReadAll(&w)
- if err != nil {
- return err
- }
- tos, err := mail.ParseAddressList(strings.Join(s.tos, ","))
- if err != nil {
- return err
- }
- op := "receive"
- if dom := strings.Split(s.from, "@"); dom[1] == cfg.Domain {
- op = "send"
- }
- hs := []string{}
- switch op {
- case "send":
- slog.Info("operation is send")
- // check auth
- if s.user.Name == "" {
- slog.Info("send error: empty name")
- return smtp.ErrAuthRequired
- }
- err = s.Send(s.from, tos, strings.NewReader(string(cont)))
- if err != nil {
- slog.Error("send error", "msg", err.Error())
- return err
- }
- hs = []string{path.Join(configPath, "hooks", "send-"+cfg.Domain)}
- case "receive":
- slog.Info("operation is receive")
- for _, to := range tos {
- domain := strings.Split(to.Address, "@")[1]
- hs = append(hs, path.Join(configPath, "hooks", "receive-"+domain))
- }
- }
- // running handlers is optional
- for _, h := range hs {
- slog.Info("running handler", "file", h)
- if f, err := os.Lstat(h); err != nil {
- slog.Warn("handler error", "err", err.Error())
- continue
- } else {
- if !f.Mode().IsRegular() {
- h, err = os.Readlink(h)
- if err != nil {
- slog.Error("read link", "domain", cfg.Domain, "error", err.Error())
- }
- }
- c := exec.Command(h)
- c.Dir = path.Join(configPath, "hooks")
- c.Stdin = strings.NewReader(string(mess))
- out := bytes.Buffer{}
- c.Stdout = &out
- if err := c.Run(); err != nil {
- slog.Error("run script", "error", err.Error(), "output", out.String())
- return err
- }
- }
- }
- return err
- }
- func (s *Session) Reset() {}
- func (s *Session) Logout() error {
- slog.Debug("logged out", "user", s.user)
- return nil
- }
- func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error {
- content, err := io.ReadAll(raw)
- if err != nil {
- return err
- }
- for _, to := range tos {
- slog.Debug("sending email", "to", to)
- // dns mx for email
- addr := strings.Split(to.Address, "@")
- mxs, err := net.LookupMX(addr[1])
- if err != nil {
- slog.Error("mx lookup", "address", addr[1])
- return err
- }
- if len(mxs) == 0 {
- slog.Error("mx lookup", "lenght", 0)
- return err
- }
- addrs := make([]string, len(tos))
- for i, to := range tos {
- addrs[i] = to.Address
- }
- server := mxs[0].Host + ":smtp"
- slog.Info("sending", "host", server, "from", from, "to", tos)
- slog.Debug("message", "data", content)
- err = smtp.SendMail(
- server,
- nil,
- from,
- addrs,
- strings.NewReader(string(content)),
- )
- if err != nil {
- return err
- }
- }
- slog.Debug("email sent")
- return nil
- }
- type backend struct{}
- func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
- return &Session{tos: []string{}}, nil
- }