123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- // Copyright 2014 The Gogs Authors. All rights reserved.
- // Copyright 2017 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package mailer
-
- import (
- "bytes"
- "context"
- "crypto/tls"
- "fmt"
- "hash/fnv"
- "io"
- "net"
- "net/smtp"
- "os"
- "os/exec"
- "strings"
- "time"
-
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- notify_service "code.gitea.io/gitea/services/notify"
-
- ntlmssp "github.com/Azure/go-ntlmssp"
- "github.com/jaytaylor/html2text"
- "gopkg.in/gomail.v2"
- )
-
- // Message mail body and log info
- type Message struct {
- Info string // Message information for log purpose.
- FromAddress string
- FromDisplayName string
- To string // Use only one recipient to prevent leaking of addresses
- ReplyTo string
- Subject string
- Date time.Time
- Body string
- Headers map[string][]string
- }
-
- // ToMessage converts a Message to gomail.Message
- func (m *Message) ToMessage() *gomail.Message {
- msg := gomail.NewMessage()
- msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName)
- msg.SetHeader("To", m.To)
- if m.ReplyTo != "" {
- msg.SetHeader("Reply-To", m.ReplyTo)
- }
- for header := range m.Headers {
- msg.SetHeader(header, m.Headers[header]...)
- }
-
- if len(setting.MailService.SubjectPrefix) > 0 {
- msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject)
- } else {
- msg.SetHeader("Subject", m.Subject)
- }
- msg.SetDateHeader("Date", m.Date)
- msg.SetHeader("X-Auto-Response-Suppress", "All")
-
- plainBody, err := html2text.FromString(m.Body)
- if err != nil || setting.MailService.SendAsPlainText {
- if strings.Contains(base.TruncateString(m.Body, 100), "<html>") {
- log.Warn("Mail contains HTML but configured to send as plain text.")
- }
- msg.SetBody("text/plain", plainBody)
- } else {
- msg.SetBody("text/plain", plainBody)
- msg.AddAlternative("text/html", m.Body)
- }
-
- if len(msg.GetHeader("Message-ID")) == 0 {
- msg.SetHeader("Message-ID", m.generateAutoMessageID())
- }
- return msg
- }
-
- // SetHeader adds additional headers to a message
- func (m *Message) SetHeader(field string, value ...string) {
- m.Headers[field] = value
- }
-
- func (m *Message) generateAutoMessageID() string {
- dateMs := m.Date.UnixNano() / 1e6
- h := fnv.New64()
- if len(m.To) > 0 {
- _, _ = h.Write([]byte(m.To))
- }
- _, _ = h.Write([]byte(m.Subject))
- _, _ = h.Write([]byte(m.Body))
- return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain)
- }
-
- // NewMessageFrom creates new mail message object with custom From header.
- func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message {
- log.Trace("NewMessageFrom (body):\n%s", body)
-
- return &Message{
- FromAddress: fromAddress,
- FromDisplayName: fromDisplayName,
- To: to,
- Subject: subject,
- Date: time.Now(),
- Body: body,
- Headers: map[string][]string{},
- }
- }
-
- // NewMessage creates new mail message object with default From header.
- func NewMessage(to, subject, body string) *Message {
- return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body)
- }
-
- type loginAuth struct {
- username, password string
- }
-
- // LoginAuth SMTP AUTH LOGIN Auth Handler
- func LoginAuth(username, password string) smtp.Auth {
- return &loginAuth{username, password}
- }
-
- // Start start SMTP login auth
- func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
- return "LOGIN", []byte{}, nil
- }
-
- // Next next step of SMTP login auth
- func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
- if more {
- switch string(fromServer) {
- case "Username:":
- return []byte(a.username), nil
- case "Password:":
- return []byte(a.password), nil
- default:
- return nil, fmt.Errorf("unknown fromServer: %s", string(fromServer))
- }
- }
- return nil, nil
- }
-
- type ntlmAuth struct {
- username, password, domain string
- domainNeeded bool
- }
-
- // NtlmAuth SMTP AUTH NTLM Auth Handler
- func NtlmAuth(username, password string) smtp.Auth {
- user, domain, domainNeeded := ntlmssp.GetDomain(username)
- return &ntlmAuth{user, password, domain, domainNeeded}
- }
-
- // Start starts SMTP NTLM Auth
- func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
- negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, "")
- return "NTLM", negotiateMessage, err
- }
-
- // Next next step of SMTP ntlm auth
- func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) {
- if more {
- if len(fromServer) == 0 {
- return nil, fmt.Errorf("ntlm ChallengeMessage is empty")
- }
- authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
- return authenticateMessage, err
- }
- return nil, nil
- }
-
- // Sender SMTP mail sender
- type smtpSender struct{}
-
- // Send send email
- func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
- opts := setting.MailService
-
- var network string
- var address string
- if opts.Protocol == "smtp+unix" {
- network = "unix"
- address = opts.SMTPAddr
- } else {
- network = "tcp"
- address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort)
- }
-
- conn, err := net.Dial(network, address)
- if err != nil {
- return fmt.Errorf("failed to establish network connection to SMTP server: %w", err)
- }
- defer conn.Close()
-
- var tlsconfig *tls.Config
- if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" {
- tlsconfig = &tls.Config{
- InsecureSkipVerify: opts.ForceTrustServerCert,
- ServerName: opts.SMTPAddr,
- }
-
- if opts.UseClientCert {
- cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile)
- if err != nil {
- return fmt.Errorf("could not load SMTP client certificate: %w", err)
- }
- tlsconfig.Certificates = []tls.Certificate{cert}
- }
- }
-
- if opts.Protocol == "smtps" {
- conn = tls.Client(conn, tlsconfig)
- }
-
- host := "localhost"
- if opts.Protocol == "smtp+unix" {
- host = opts.SMTPAddr
- }
- client, err := smtp.NewClient(conn, host)
- if err != nil {
- return fmt.Errorf("could not initiate SMTP session: %w", err)
- }
-
- if opts.EnableHelo {
- hostname := opts.HeloHostname
- if len(hostname) == 0 {
- hostname, err = os.Hostname()
- if err != nil {
- return fmt.Errorf("could not retrieve system hostname: %w", err)
- }
- }
-
- if err = client.Hello(hostname); err != nil {
- return fmt.Errorf("failed to issue HELO command: %w", err)
- }
- }
-
- if opts.Protocol == "smtp+starttls" {
- hasStartTLS, _ := client.Extension("STARTTLS")
- if hasStartTLS {
- if err = client.StartTLS(tlsconfig); err != nil {
- return fmt.Errorf("failed to start TLS connection: %w", err)
- }
- } else {
- log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP")
- }
- }
-
- canAuth, options := client.Extension("AUTH")
- if len(opts.User) > 0 {
- if !canAuth {
- return fmt.Errorf("SMTP server does not support AUTH, but credentials provided")
- }
-
- var auth smtp.Auth
-
- if strings.Contains(options, "CRAM-MD5") {
- auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd)
- } else if strings.Contains(options, "PLAIN") {
- auth = smtp.PlainAuth("", opts.User, opts.Passwd, host)
- } else if strings.Contains(options, "LOGIN") {
- // Patch for AUTH LOGIN
- auth = LoginAuth(opts.User, opts.Passwd)
- } else if strings.Contains(options, "NTLM") {
- auth = NtlmAuth(opts.User, opts.Passwd)
- }
-
- if auth != nil {
- if err = client.Auth(auth); err != nil {
- return fmt.Errorf("failed to authenticate SMTP: %w", err)
- }
- }
- }
-
- if opts.OverrideEnvelopeFrom {
- if err = client.Mail(opts.EnvelopeFrom); err != nil {
- return fmt.Errorf("failed to issue MAIL command: %w", err)
- }
- } else {
- if err = client.Mail(from); err != nil {
- return fmt.Errorf("failed to issue MAIL command: %w", err)
- }
- }
-
- for _, rec := range to {
- if err = client.Rcpt(rec); err != nil {
- return fmt.Errorf("failed to issue RCPT command: %w", err)
- }
- }
-
- w, err := client.Data()
- if err != nil {
- return fmt.Errorf("failed to issue DATA command: %w", err)
- } else if _, err = msg.WriteTo(w); err != nil {
- return fmt.Errorf("SMTP write failed: %w", err)
- } else if err = w.Close(); err != nil {
- return fmt.Errorf("SMTP close failed: %w", err)
- }
-
- return client.Quit()
- }
-
- // Sender sendmail mail sender
- type sendmailSender struct{}
-
- // Send send email
- func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
- var err error
- var closeError error
- var waitError error
-
- envelopeFrom := from
- if setting.MailService.OverrideEnvelopeFrom {
- envelopeFrom = setting.MailService.EnvelopeFrom
- }
-
- args := []string{"-f", envelopeFrom, "-i"}
- args = append(args, setting.MailService.SendmailArgs...)
- args = append(args, to...)
- log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args)
-
- desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args)
-
- ctx, _, finished := process.GetManager().AddContextTimeout(graceful.GetManager().HammerContext(), setting.MailService.SendmailTimeout, desc)
- defer finished()
-
- cmd := exec.CommandContext(ctx, setting.MailService.SendmailPath, args...)
- pipe, err := cmd.StdinPipe()
- if err != nil {
- return err
- }
- process.SetSysProcAttribute(cmd)
-
- if err = cmd.Start(); err != nil {
- _ = pipe.Close()
- return err
- }
-
- if setting.MailService.SendmailConvertCRLF {
- buf := &strings.Builder{}
- _, err = msg.WriteTo(buf)
- if err == nil {
- _, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String())
- }
- } else {
- _, err = msg.WriteTo(pipe)
- }
-
- // we MUST close the pipe or sendmail will hang waiting for more of the message
- // Also we should wait on our sendmail command even if something fails
- closeError = pipe.Close()
- waitError = cmd.Wait()
- if err != nil {
- return err
- } else if closeError != nil {
- return closeError
- } else {
- return waitError
- }
- }
-
- // Sender sendmail mail sender
- type dummySender struct{}
-
- // Send send email
- func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
- buf := bytes.Buffer{}
- if _, err := msg.WriteTo(&buf); err != nil {
- return err
- }
- log.Info("Mail From: %s To: %v Body: %s", from, to, buf.String())
- return nil
- }
-
- var mailQueue *queue.WorkerPoolQueue[*Message]
-
- // Sender sender for sending mail synchronously
- var Sender gomail.Sender
-
- // NewContext start mail queue service
- func NewContext(ctx context.Context) {
- // Need to check if mailQueue is nil because in during reinstall (user had installed
- // before but switched install lock off), this function will be called again
- // while mail queue is already processing tasks, and produces a race condition.
- if setting.MailService == nil || mailQueue != nil {
- return
- }
-
- if setting.Service.EnableNotifyMail {
- notify_service.RegisterNotifier(NewNotifier())
- }
-
- switch setting.MailService.Protocol {
- case "sendmail":
- Sender = &sendmailSender{}
- case "dummy":
- Sender = &dummySender{}
- default:
- Sender = &smtpSender{}
- }
-
- subjectTemplates, bodyTemplates = templates.Mailer(ctx)
-
- mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*Message) []*Message {
- for _, msg := range items {
- gomailMsg := msg.ToMessage()
- log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info)
- if err := gomail.Send(Sender, gomailMsg); err != nil {
- log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err)
- } else {
- log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info)
- }
- }
- return nil
- })
- if mailQueue == nil {
- log.Fatal("Unable to create mail queue")
- }
- go graceful.GetManager().RunWithCancel(mailQueue)
- }
-
- // SendAsync send emails asynchronously (make it mockable)
- var SendAsync = sendAsync
-
- func sendAsync(msgs ...*Message) {
- if setting.MailService == nil {
- log.Error("Mailer: SendAsync is being invoked but mail service hasn't been initialized")
- return
- }
-
- go func() {
- for _, msg := range msgs {
- _ = mailQueue.Push(msg)
- }
- }()
- }
|