You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mailer.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2017 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package mailer
  5. import (
  6. "bytes"
  7. "context"
  8. "crypto/tls"
  9. "fmt"
  10. "hash/fnv"
  11. "io"
  12. "net"
  13. "net/smtp"
  14. "os"
  15. "os/exec"
  16. "strings"
  17. "time"
  18. "code.gitea.io/gitea/modules/base"
  19. "code.gitea.io/gitea/modules/graceful"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/process"
  22. "code.gitea.io/gitea/modules/queue"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/templates"
  25. notify_service "code.gitea.io/gitea/services/notify"
  26. ntlmssp "github.com/Azure/go-ntlmssp"
  27. "github.com/jaytaylor/html2text"
  28. "gopkg.in/gomail.v2"
  29. )
  30. // Message mail body and log info
  31. type Message struct {
  32. Info string // Message information for log purpose.
  33. FromAddress string
  34. FromDisplayName string
  35. To string // Use only one recipient to prevent leaking of addresses
  36. ReplyTo string
  37. Subject string
  38. Date time.Time
  39. Body string
  40. Headers map[string][]string
  41. }
  42. // ToMessage converts a Message to gomail.Message
  43. func (m *Message) ToMessage() *gomail.Message {
  44. msg := gomail.NewMessage()
  45. msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName)
  46. msg.SetHeader("To", m.To)
  47. if m.ReplyTo != "" {
  48. msg.SetHeader("Reply-To", m.ReplyTo)
  49. }
  50. for header := range m.Headers {
  51. msg.SetHeader(header, m.Headers[header]...)
  52. }
  53. if len(setting.MailService.SubjectPrefix) > 0 {
  54. msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject)
  55. } else {
  56. msg.SetHeader("Subject", m.Subject)
  57. }
  58. msg.SetDateHeader("Date", m.Date)
  59. msg.SetHeader("X-Auto-Response-Suppress", "All")
  60. plainBody, err := html2text.FromString(m.Body)
  61. if err != nil || setting.MailService.SendAsPlainText {
  62. if strings.Contains(base.TruncateString(m.Body, 100), "<html>") {
  63. log.Warn("Mail contains HTML but configured to send as plain text.")
  64. }
  65. msg.SetBody("text/plain", plainBody)
  66. } else {
  67. msg.SetBody("text/plain", plainBody)
  68. msg.AddAlternative("text/html", m.Body)
  69. }
  70. if len(msg.GetHeader("Message-ID")) == 0 {
  71. msg.SetHeader("Message-ID", m.generateAutoMessageID())
  72. }
  73. return msg
  74. }
  75. // SetHeader adds additional headers to a message
  76. func (m *Message) SetHeader(field string, value ...string) {
  77. m.Headers[field] = value
  78. }
  79. func (m *Message) generateAutoMessageID() string {
  80. dateMs := m.Date.UnixNano() / 1e6
  81. h := fnv.New64()
  82. if len(m.To) > 0 {
  83. _, _ = h.Write([]byte(m.To))
  84. }
  85. _, _ = h.Write([]byte(m.Subject))
  86. _, _ = h.Write([]byte(m.Body))
  87. return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain)
  88. }
  89. // NewMessageFrom creates new mail message object with custom From header.
  90. func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message {
  91. log.Trace("NewMessageFrom (body):\n%s", body)
  92. return &Message{
  93. FromAddress: fromAddress,
  94. FromDisplayName: fromDisplayName,
  95. To: to,
  96. Subject: subject,
  97. Date: time.Now(),
  98. Body: body,
  99. Headers: map[string][]string{},
  100. }
  101. }
  102. // NewMessage creates new mail message object with default From header.
  103. func NewMessage(to, subject, body string) *Message {
  104. return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body)
  105. }
  106. type loginAuth struct {
  107. username, password string
  108. }
  109. // LoginAuth SMTP AUTH LOGIN Auth Handler
  110. func LoginAuth(username, password string) smtp.Auth {
  111. return &loginAuth{username, password}
  112. }
  113. // Start start SMTP login auth
  114. func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
  115. return "LOGIN", []byte{}, nil
  116. }
  117. // Next next step of SMTP login auth
  118. func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
  119. if more {
  120. switch string(fromServer) {
  121. case "Username:":
  122. return []byte(a.username), nil
  123. case "Password:":
  124. return []byte(a.password), nil
  125. default:
  126. return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer))
  127. }
  128. }
  129. return nil, nil
  130. }
  131. type ntlmAuth struct {
  132. username, password, domain string
  133. domainNeeded bool
  134. }
  135. // NtlmAuth SMTP AUTH NTLM Auth Handler
  136. func NtlmAuth(username, password string) smtp.Auth {
  137. user, domain, domainNeeded := ntlmssp.GetDomain(username)
  138. return &ntlmAuth{user, password, domain, domainNeeded}
  139. }
  140. // Start starts SMTP NTLM Auth
  141. func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
  142. negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "")
  143. return "NTLM", negotiateMessage, err
  144. }
  145. // Next next step of SMTP ntlm auth
  146. func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) {
  147. if more {
  148. if len(fromServer) == 0 {
  149. return nil, fmt.Errorf("ntlm ChallengeMessage is empty")
  150. }
  151. authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
  152. return authenticateMessage, err
  153. }
  154. return nil, nil
  155. }
  156. // Sender SMTP mail sender
  157. type smtpSender struct{}
  158. // Send send email
  159. func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
  160. opts := setting.MailService
  161. var network string
  162. var address string
  163. if opts.Protocol == "smtp+unix" {
  164. network = "unix"
  165. address = opts.SMTPAddr
  166. } else {
  167. network = "tcp"
  168. address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort)
  169. }
  170. conn, err := net.Dial(network, address)
  171. if err != nil {
  172. return fmt.Errorf("failed to establish network connection to SMTP server: %w", err)
  173. }
  174. defer conn.Close()
  175. var tlsconfig *tls.Config
  176. if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" {
  177. tlsconfig = &tls.Config{
  178. InsecureSkipVerify: opts.ForceTrustServerCert,
  179. ServerName: opts.SMTPAddr,
  180. }
  181. if opts.UseClientCert {
  182. cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile)
  183. if err != nil {
  184. return fmt.Errorf("could not load SMTP client certificate: %w", err)
  185. }
  186. tlsconfig.Certificates = []tls.Certificate{cert}
  187. }
  188. }
  189. if opts.Protocol == "smtps" {
  190. conn = tls.Client(conn, tlsconfig)
  191. }
  192. host := "localhost"
  193. if opts.Protocol == "smtp+unix" {
  194. host = opts.SMTPAddr
  195. }
  196. client, err := smtp.NewClient(conn, host)
  197. if err != nil {
  198. return fmt.Errorf("could not initiate SMTP session: %w", err)
  199. }
  200. if opts.EnableHelo {
  201. hostname := opts.HeloHostname
  202. if len(hostname) == 0 {
  203. hostname, err = os.Hostname()
  204. if err != nil {
  205. return fmt.Errorf("could not retrieve system hostname: %w", err)
  206. }
  207. }
  208. if err = client.Hello(hostname); err != nil {
  209. return fmt.Errorf("failed to issue HELO command: %w", err)
  210. }
  211. }
  212. if opts.Protocol == "smtp+starttls" {
  213. hasStartTLS, _ := client.Extension("STARTTLS")
  214. if hasStartTLS {
  215. if err = client.StartTLS(tlsconfig); err != nil {
  216. return fmt.Errorf("failed to start TLS connection: %w", err)
  217. }
  218. } else {
  219. log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP")
  220. }
  221. }
  222. canAuth, options := client.Extension("AUTH")
  223. if len(opts.User) > 0 {
  224. if !canAuth {
  225. return fmt.Errorf("SMTP server does not support AUTH, but credentials provided")
  226. }
  227. var auth smtp.Auth
  228. if strings.Contains(options, "CRAM-MD5") {
  229. auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd)
  230. } else if strings.Contains(options, "PLAIN") {
  231. auth = smtp.PlainAuth("", opts.User, opts.Passwd, host)
  232. } else if strings.Contains(options, "LOGIN") {
  233. // Patch for AUTH LOGIN
  234. auth = LoginAuth(opts.User, opts.Passwd)
  235. } else if strings.Contains(options, "NTLM") {
  236. auth = NtlmAuth(opts.User, opts.Passwd)
  237. }
  238. if auth != nil {
  239. if err = client.Auth(auth); err != nil {
  240. return fmt.Errorf("failed to authenticate SMTP: %w", err)
  241. }
  242. }
  243. }
  244. if opts.OverrideEnvelopeFrom {
  245. if err = client.Mail(opts.EnvelopeFrom); err != nil {
  246. return fmt.Errorf("failed to issue MAIL command: %w", err)
  247. }
  248. } else {
  249. if err = client.Mail(from); err != nil {
  250. return fmt.Errorf("failed to issue MAIL command: %w", err)
  251. }
  252. }
  253. for _, rec := range to {
  254. if err = client.Rcpt(rec); err != nil {
  255. return fmt.Errorf("failed to issue RCPT command: %w", err)
  256. }
  257. }
  258. w, err := client.Data()
  259. if err != nil {
  260. return fmt.Errorf("failed to issue DATA command: %w", err)
  261. } else if _, err = msg.WriteTo(w); err != nil {
  262. return fmt.Errorf("SMTP write failed: %w", err)
  263. } else if err = w.Close(); err != nil {
  264. return fmt.Errorf("SMTP close failed: %w", err)
  265. }
  266. return client.Quit()
  267. }
  268. // Sender sendmail mail sender
  269. type sendmailSender struct{}
  270. // Send send email
  271. func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
  272. var err error
  273. var closeError error
  274. var waitError error
  275. envelopeFrom := from
  276. if setting.MailService.OverrideEnvelopeFrom {
  277. envelopeFrom = setting.MailService.EnvelopeFrom
  278. }
  279. args := []string{"-f", envelopeFrom, "-i"}
  280. args = append(args, setting.MailService.SendmailArgs...)
  281. args = append(args, to...)
  282. log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args)
  283. desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args)
  284. ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc)
  285. defer finished()
  286. cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...)
  287. pipe, err := cmd.StdinPipe()
  288. if err != nil {
  289. return err
  290. }
  291. process.SetSysProcAttribute(cmd)
  292. if err = cmd.Start(); err != nil {
  293. _ = pipe.Close()
  294. return err
  295. }
  296. if setting.MailService.SendmailConvertCRLF {
  297. buf := &strings.Builder{}
  298. _, err = msg.WriteTo(buf)
  299. if err == nil {
  300. _, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String())
  301. }
  302. } else {
  303. _, err = msg.WriteTo(pipe)
  304. }
  305. // we MUST close the pipe or sendmail will hang waiting for more of the message
  306. // Also we should wait on our sendmail command even if something fails
  307. closeError = pipe.Close()
  308. waitError = cmd.Wait()
  309. if err != nil {
  310. return err
  311. } else if closeError != nil {
  312. return closeError
  313. } else {
  314. return waitError
  315. }
  316. }
  317. // Sender sendmail mail sender
  318. type dummySender struct{}
  319. // Send send email
  320. func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
  321. buf := bytes.Buffer{}
  322. if _, err := msg.WriteTo(&buf); err != nil {
  323. return err
  324. }
  325. log.Info("Mail From: %s To: %v Body: %s", from, to, buf.String())
  326. return nil
  327. }
  328. var mailQueue *queue.WorkerPoolQueue[*Message]
  329. // Sender sender for sending mail synchronously
  330. var Sender gomail.Sender
  331. // NewContext start mail queue service
  332. func NewContext(ctx context.Context) {
  333. // Need to check if mailQueue is nil because in during reinstall (user had installed
  334. // before but switched install lock off), this function will be called again
  335. // while mail queue is already processing tasks, and produces a race condition.
  336. if setting.MailService == nil || mailQueue != nil {
  337. return
  338. }
  339. if setting.Service.EnableNotifyMail {
  340. notify_service.RegisterNotifier(NewNotifier())
  341. }
  342. switch setting.MailService.Protocol {
  343. case "sendmail":
  344. Sender = &sendmailSender{}
  345. case "dummy":
  346. Sender = &dummySender{}
  347. default:
  348. Sender = &smtpSender{}
  349. }
  350. subjectTemplates, bodyTemplates = templates.Mailer(ctx)
  351. mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*Message) []*Message {
  352. for _, msg := range items {
  353. gomailMsg := msg.ToMessage()
  354. log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info)
  355. if err := gomail.Send(Sender, gomailMsg); err != nil {
  356. log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err)
  357. } else {
  358. log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info)
  359. }
  360. }
  361. return nil
  362. })
  363. if mailQueue == nil {
  364. log.Fatal("Unable to create mail queue")
  365. }
  366. go graceful.GetManager().RunWithCancel(mailQueue)
  367. }
  368. // SendAsync send emails asynchronously (make it mockable)
  369. var SendAsync = sendAsync
  370. func sendAsync(msgs ...*Message) {
  371. if setting.MailService == nil {
  372. log.Error("Mailer: SendAsync is being invoked but mail service hasn't been initialized")
  373. return
  374. }
  375. go func() {
  376. for _, msg := range msgs {
  377. _ = mailQueue.Push(msg)
  378. }
  379. }()
  380. }