code.gitea.io/gitea@v1.22.3/services/mailer/mailer.go (about)

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