list

server

This is the main dovel repository, it has the Go code to run dovel SMTP server.

curl -O https://dovel.email/server.tar.gz tar.gz

backend.go

  1. package main
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "net"
  8. "net/mail"
  9. "os"
  10. "os/exec"
  11. "path"
  12. "strings"
  13. "time"
  14. "github.com/emersion/go-mbox"
  15. "github.com/emersion/go-smtp"
  16. )
  17. // A Session is returned after EHLO.
  18. type Session struct {
  19. user User
  20. from string
  21. tos []string
  22. }
  23. func (s *Session) AuthPlain(username, password string) error {
  24. slog.Debug("authenticating", "user", username, "pass", password)
  25. if !v.Validate(username, password) {
  26. slog.Warn("unauthorized", "user", username)
  27. return fmt.Errorf("user not authorized")
  28. }
  29. slog.Debug("authorized", "user", username)
  30. s.user = v.GetUser(username)
  31. return nil
  32. }
  33. func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
  34. slog.Debug("received mail", "from", from)
  35. s.from = from
  36. return nil
  37. }
  38. func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
  39. slog.Debug("received rcpt to", "to", to)
  40. s.tos = append(s.tos, to)
  41. return nil
  42. }
  43. func (s *Session) Data(raw io.Reader) error {
  44. cont, err := io.ReadAll(raw)
  45. if err != nil {
  46. return err
  47. }
  48. slog.Debug("received data", "data", cont)
  49. w := bytes.Buffer{}
  50. box := mbox.NewWriter(&w)
  51. boxW, err := box.CreateMessage(s.from, time.Now())
  52. if err != nil {
  53. return err
  54. }
  55. boxW.Write(cont)
  56. box.Close()
  57. mess, err := io.ReadAll(&w)
  58. if err != nil {
  59. return err
  60. }
  61. tos, err := mail.ParseAddressList(strings.Join(s.tos, ","))
  62. if err != nil {
  63. return err
  64. }
  65. op := "receive"
  66. if dom := strings.Split(s.from, "@"); dom[1] == cfg.Domain {
  67. op = "send"
  68. }
  69. hs := []string{}
  70. switch op {
  71. case "send":
  72. slog.Info("operation is send")
  73. // check auth
  74. if s.user.Name == "" {
  75. slog.Info("send error: empty name")
  76. return smtp.ErrAuthRequired
  77. }
  78. err = s.Send(s.from, tos, strings.NewReader(string(cont)))
  79. if err != nil {
  80. slog.Error("send error", "msg", err.Error())
  81. return err
  82. }
  83. hs = []string{path.Join(configPath, "hooks", "send-"+cfg.Domain)}
  84. case "receive":
  85. slog.Info("operation is receive")
  86. for _, to := range tos {
  87. domain := strings.Split(to.Address, "@")[1]
  88. hs = append(hs, path.Join(configPath, "hooks", "receive-"+domain))
  89. }
  90. }
  91. // running handlers is optional
  92. for _, h := range hs {
  93. slog.Info("running handler", "file", h)
  94. if f, err := os.Lstat(h); err != nil {
  95. slog.Warn("handler error", "err", err.Error())
  96. continue
  97. } else {
  98. if !f.Mode().IsRegular() {
  99. h, err = os.Readlink(h)
  100. if err != nil {
  101. slog.Error("read link", "domain", cfg.Domain, "error", err.Error())
  102. }
  103. }
  104. c := exec.Command(h)
  105. c.Dir = path.Join(configPath, "hooks")
  106. c.Stdin = strings.NewReader(string(mess))
  107. out := bytes.Buffer{}
  108. c.Stdout = &out
  109. if err := c.Run(); err != nil {
  110. slog.Error("run script", "error", err.Error(), "output", out.String())
  111. return err
  112. }
  113. }
  114. }
  115. return err
  116. }
  117. func (s *Session) Reset() {}
  118. func (s *Session) Logout() error {
  119. slog.Debug("logged out", "user", s.user)
  120. return nil
  121. }
  122. func (s *Session) Send(from string, tos []*mail.Address, raw io.Reader) error {
  123. content, err := io.ReadAll(raw)
  124. if err != nil {
  125. return err
  126. }
  127. for _, to := range tos {
  128. slog.Debug("sending email", "to", to)
  129. // dns mx for email
  130. addr := strings.Split(to.Address, "@")
  131. mxs, err := net.LookupMX(addr[1])
  132. if err != nil {
  133. slog.Error("mx lookup", "address", addr[1])
  134. return err
  135. }
  136. if len(mxs) == 0 {
  137. slog.Error("mx lookup", "lenght", 0)
  138. return err
  139. }
  140. addrs := make([]string, len(tos))
  141. for i, to := range tos {
  142. addrs[i] = to.Address
  143. }
  144. server := mxs[0].Host + ":smtp"
  145. slog.Info("sending", "host", server, "from", from, "to", tos)
  146. slog.Debug("message", "data", content)
  147. err = smtp.SendMail(
  148. server,
  149. nil,
  150. from,
  151. addrs,
  152. strings.NewReader(string(content)),
  153. )
  154. if err != nil {
  155. return err
  156. }
  157. }
  158. slog.Debug("email sent")
  159. return nil
  160. }
  161. type backend struct{}
  162. func (b backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
  163. return &Session{tos: []string{}}, nil
  164. }