smtp_server.go 9.4 KB

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