smtp_server.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package server
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "github.com/emersion/go-smtp"
  8. "io"
  9. "mime"
  10. "mime/multipart"
  11. "net"
  12. "net/http"
  13. "net/http/httptest"
  14. "net/mail"
  15. "strings"
  16. "sync"
  17. )
  18. var (
  19. errInvalidDomain = errors.New("invalid domain")
  20. errInvalidAddress = errors.New("invalid address")
  21. errInvalidTopic = errors.New("invalid topic")
  22. errTooManyRecipients = errors.New("too many recipients")
  23. errUnsupportedContentType = errors.New("unsupported content type")
  24. )
  25. // smtpBackend implements SMTP server methods.
  26. type smtpBackend struct {
  27. config *Config
  28. handler func(http.ResponseWriter, *http.Request)
  29. success int64
  30. failure int64
  31. mu sync.Mutex
  32. }
  33. var _ smtp.Backend = (*smtpBackend)(nil)
  34. var _ smtp.Session = (*smtpSession)(nil)
  35. func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend {
  36. return &smtpBackend{
  37. config: conf,
  38. handler: handler,
  39. }
  40. }
  41. func (b *smtpBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
  42. logem(conn).Debug("Incoming mail")
  43. return &smtpSession{backend: b, conn: conn}, nil
  44. }
  45. func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
  46. b.mu.Lock()
  47. defer b.mu.Unlock()
  48. return b.success + b.failure, b.success, b.failure
  49. }
  50. // smtpSession is returned after EHLO.
  51. type smtpSession struct {
  52. backend *smtpBackend
  53. conn *smtp.Conn
  54. topic string
  55. mu sync.Mutex
  56. }
  57. func (s *smtpSession) AuthPlain(username, _ string) error {
  58. logem(s.conn).Field("smtp_username", username).Debug("AUTH PLAIN (with username %s)", username)
  59. return nil
  60. }
  61. func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
  62. logem(s.conn).Field("smtp_mail_from", from).Debug("MAIL FROM: %s", from)
  63. return nil
  64. }
  65. func (s *smtpSession) Rcpt(to string) error {
  66. logem(s.conn).Field("smtp_rcpt_to", to).Debug("RCPT TO: %s", to)
  67. return s.withFailCount(func() error {
  68. conf := s.backend.config
  69. addressList, err := mail.ParseAddressList(to)
  70. if err != nil {
  71. return err
  72. } else if len(addressList) != 1 {
  73. return errTooManyRecipients
  74. }
  75. to = addressList[0].Address
  76. if !strings.HasSuffix(to, "@"+conf.SMTPServerDomain) {
  77. return errInvalidDomain
  78. }
  79. to = strings.TrimSuffix(to, "@"+conf.SMTPServerDomain)
  80. if conf.SMTPServerAddrPrefix != "" {
  81. if !strings.HasPrefix(to, conf.SMTPServerAddrPrefix) {
  82. return errInvalidAddress
  83. }
  84. to = strings.TrimPrefix(to, conf.SMTPServerAddrPrefix)
  85. }
  86. if !topicRegex.MatchString(to) {
  87. return errInvalidTopic
  88. }
  89. s.mu.Lock()
  90. s.topic = to
  91. s.mu.Unlock()
  92. return nil
  93. })
  94. }
  95. func (s *smtpSession) Data(r io.Reader) error {
  96. return s.withFailCount(func() error {
  97. conf := s.backend.config
  98. b, err := io.ReadAll(r) // Protected by MaxMessageBytes
  99. if err != nil {
  100. return err
  101. }
  102. ev := logem(s.conn)
  103. if ev.IsTrace() {
  104. ev.Field("smtp_data", string(b)).Trace("DATA")
  105. } else if ev.IsDebug() {
  106. ev.Field("smtp_data_len", len(b)).Debug("DATA")
  107. }
  108. msg, err := mail.ReadMessage(bytes.NewReader(b))
  109. if err != nil {
  110. return err
  111. }
  112. body, err := readMailBody(msg)
  113. if err != nil {
  114. return err
  115. }
  116. body = strings.TrimSpace(body)
  117. if len(body) > conf.MessageLimit {
  118. body = body[:conf.MessageLimit]
  119. }
  120. m := newDefaultMessage(s.topic, body)
  121. subject := strings.TrimSpace(msg.Header.Get("Subject"))
  122. if subject != "" {
  123. dec := mime.WordDecoder{}
  124. subject, err := dec.DecodeHeader(subject)
  125. if err != nil {
  126. return err
  127. }
  128. m.Title = subject
  129. }
  130. if m.Title != "" && m.Message == "" {
  131. m.Message = m.Title // Flip them, this makes more sense
  132. m.Title = ""
  133. }
  134. if err := s.publishMessage(m); err != nil {
  135. return err
  136. }
  137. s.backend.mu.Lock()
  138. s.backend.success++
  139. s.backend.mu.Unlock()
  140. return nil
  141. })
  142. }
  143. func (s *smtpSession) publishMessage(m *message) error {
  144. // Extract remote address (for rate limiting)
  145. remoteAddr, _, err := net.SplitHostPort(s.conn.Conn().RemoteAddr().String())
  146. if err != nil {
  147. remoteAddr = s.conn.Conn().RemoteAddr().String()
  148. }
  149. // Call HTTP handler with fake HTTP request
  150. url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic)
  151. req, err := http.NewRequest("POST", url, strings.NewReader(m.Message))
  152. req.RequestURI = "/" + m.Topic // just for the logs
  153. req.RemoteAddr = remoteAddr // rate limiting!!
  154. req.Header.Set("X-Forwarded-For", remoteAddr)
  155. if err != nil {
  156. return err
  157. }
  158. if m.Title != "" {
  159. req.Header.Set("Title", m.Title)
  160. }
  161. rr := httptest.NewRecorder()
  162. s.backend.handler(rr, req)
  163. if rr.Code != http.StatusOK {
  164. return errors.New("error: " + rr.Body.String())
  165. }
  166. return nil
  167. }
  168. func (s *smtpSession) Reset() {
  169. s.mu.Lock()
  170. s.topic = ""
  171. s.mu.Unlock()
  172. }
  173. func (s *smtpSession) Logout() error {
  174. return nil
  175. }
  176. func (s *smtpSession) withFailCount(fn func() error) error {
  177. err := fn()
  178. s.backend.mu.Lock()
  179. defer s.backend.mu.Unlock()
  180. if err != nil {
  181. // Almost all of these errors are parse errors, and user input errors.
  182. // We do not want to spam the log with WARN messages.
  183. logem(s.conn).Err(err).Debug("Incoming mail error")
  184. s.backend.failure++
  185. }
  186. return err
  187. }
  188. func readMailBody(msg *mail.Message) (string, error) {
  189. if msg.Header.Get("Content-Type") == "" {
  190. return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding"))
  191. }
  192. contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
  193. if err != nil {
  194. return "", err
  195. }
  196. if strings.ToLower(contentType) == "text/plain" {
  197. return readPlainTextMailBody(msg.Body, msg.Header.Get("Content-Transfer-Encoding"))
  198. } else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
  199. return readMultipartMailBody(msg, params)
  200. }
  201. return "", errUnsupportedContentType
  202. }
  203. func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) {
  204. mr := multipart.NewReader(msg.Body, params["boundary"])
  205. for {
  206. part, err := mr.NextPart()
  207. if err != nil { // may be io.EOF
  208. return "", err
  209. }
  210. partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
  211. if err != nil {
  212. return "", err
  213. } else if strings.ToLower(partContentType) != "text/plain" {
  214. continue
  215. }
  216. return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
  217. }
  218. }
  219. func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) {
  220. if strings.ToLower(transferEncoding) == "base64" {
  221. reader = base64.NewDecoder(base64.StdEncoding, reader)
  222. }
  223. body, err := io.ReadAll(reader)
  224. if err != nil {
  225. return "", err
  226. }
  227. return string(body), nil
  228. }