smtp_server.go 7.3 KB

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