smtp_server.go 6.1 KB

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