smtp_server.go 6.3 KB

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