github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/services/mailservice/mail.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package mailservice
     5  
     6  import (
     7  	"context"
     8  	"crypto/tls"
     9  	"io"
    10  	"mime"
    11  	"net"
    12  	"net/mail"
    13  	"net/smtp"
    14  	"time"
    15  
    16  	"github.com/jaytaylor/html2text"
    17  	"github.com/pkg/errors"
    18  	gomail "gopkg.in/mail.v2"
    19  
    20  	"github.com/mattermost/mattermost-server/v5/mlog"
    21  )
    22  
    23  const (
    24  	TLS      = "TLS"
    25  	StartTLS = "STARTTLS"
    26  )
    27  
    28  type SMTPConfig struct {
    29  	ConnectionSecurity                string
    30  	SkipServerCertificateVerification bool
    31  	Hostname                          string
    32  	ServerName                        string
    33  	Server                            string
    34  	Port                              string
    35  	ServerTimeout                     int
    36  	Username                          string
    37  	Password                          string
    38  	EnableSMTPAuth                    bool
    39  	SendEmailNotifications            bool
    40  	FeedbackName                      string
    41  	FeedbackEmail                     string
    42  	ReplyToAddress                    string
    43  }
    44  
    45  type mailData struct {
    46  	mimeTo        string
    47  	smtpTo        string
    48  	from          mail.Address
    49  	cc            string
    50  	replyTo       mail.Address
    51  	subject       string
    52  	htmlBody      string
    53  	embeddedFiles map[string]io.Reader
    54  	mimeHeaders   map[string]string
    55  }
    56  
    57  // smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
    58  //
    59  type smtpClient interface {
    60  	Mail(string) error
    61  	Rcpt(string) error
    62  	Data() (io.WriteCloser, error)
    63  }
    64  
    65  func encodeRFC2047Word(s string) string {
    66  	return mime.BEncoding.Encode("utf-8", s)
    67  }
    68  
    69  type authChooser struct {
    70  	smtp.Auth
    71  	config *SMTPConfig
    72  }
    73  
    74  func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
    75  	smtpAddress := a.config.ServerName + ":" + a.config.Port
    76  	a.Auth = LoginAuth(a.config.Username, a.config.Password, smtpAddress)
    77  	for _, method := range server.Auth {
    78  		if method == "PLAIN" {
    79  			a.Auth = smtp.PlainAuth("", a.config.Username, a.config.Password, a.config.ServerName+":"+a.config.Port)
    80  			break
    81  		}
    82  	}
    83  	return a.Auth.Start(server)
    84  }
    85  
    86  type loginAuth struct {
    87  	username, password, host string
    88  }
    89  
    90  func LoginAuth(username, password, host string) smtp.Auth {
    91  	return &loginAuth{username, password, host}
    92  }
    93  
    94  func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    95  	if !server.TLS {
    96  		return "", nil, errors.New("unencrypted connection")
    97  	}
    98  
    99  	if server.Name != a.host {
   100  		return "", nil, errors.New("wrong host name")
   101  	}
   102  
   103  	return "LOGIN", []byte{}, nil
   104  }
   105  
   106  func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
   107  	if more {
   108  		switch string(fromServer) {
   109  		case "Username:":
   110  			return []byte(a.username), nil
   111  		case "Password:":
   112  			return []byte(a.password), nil
   113  		default:
   114  			return nil, errors.New("Unknown fromServer")
   115  		}
   116  	}
   117  	return nil, nil
   118  }
   119  
   120  func ConnectToSMTPServerAdvanced(config *SMTPConfig) (net.Conn, error) {
   121  	var conn net.Conn
   122  	var err error
   123  
   124  	smtpAddress := config.Server + ":" + config.Port
   125  	dialer := &net.Dialer{
   126  		Timeout: time.Duration(config.ServerTimeout) * time.Second,
   127  	}
   128  
   129  	if config.ConnectionSecurity == TLS {
   130  		tlsconfig := &tls.Config{
   131  			InsecureSkipVerify: config.SkipServerCertificateVerification,
   132  			ServerName:         config.ServerName,
   133  		}
   134  
   135  		conn, err = tls.DialWithDialer(dialer, "tcp", smtpAddress, tlsconfig)
   136  		if err != nil {
   137  			return nil, errors.Wrap(err, "unable to connect to the SMTP server through TLS")
   138  		}
   139  	} else {
   140  		conn, err = dialer.Dial("tcp", smtpAddress)
   141  		if err != nil {
   142  			return nil, errors.Wrap(err, "unable to connect to the SMTP server")
   143  		}
   144  	}
   145  
   146  	return conn, nil
   147  }
   148  
   149  func ConnectToSMTPServer(config *SMTPConfig) (net.Conn, error) {
   150  	return ConnectToSMTPServerAdvanced(config)
   151  }
   152  
   153  func NewSMTPClientAdvanced(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
   154  	ctx, cancel := context.WithCancel(ctx)
   155  	defer cancel()
   156  
   157  	var c *smtp.Client
   158  	ec := make(chan error)
   159  	go func() {
   160  		var err error
   161  		c, err = smtp.NewClient(conn, config.ServerName+":"+config.Port)
   162  		if err != nil {
   163  			ec <- err
   164  			return
   165  		}
   166  		cancel()
   167  	}()
   168  
   169  	select {
   170  	case <-ctx.Done():
   171  		err := ctx.Err()
   172  		if err != nil && err.Error() != "context canceled" {
   173  			return nil, errors.Wrap(err, "unable to connect to the SMTP server")
   174  		}
   175  	case err := <-ec:
   176  		return nil, errors.Wrap(err, "unable to connect to the SMTP server")
   177  	}
   178  
   179  	if config.Hostname != "" {
   180  		err := c.Hello(config.Hostname)
   181  		if err != nil {
   182  			return nil, errors.Wrap(err, "unable to send hello message")
   183  		}
   184  	}
   185  
   186  	if config.ConnectionSecurity == StartTLS {
   187  		tlsconfig := &tls.Config{
   188  			InsecureSkipVerify: config.SkipServerCertificateVerification,
   189  			ServerName:         config.ServerName,
   190  		}
   191  		c.StartTLS(tlsconfig)
   192  	}
   193  
   194  	if config.EnableSMTPAuth {
   195  		if err := c.Auth(&authChooser{config: config}); err != nil {
   196  			return nil, errors.Wrap(err, "authentication failed")
   197  		}
   198  	}
   199  	return c, nil
   200  }
   201  
   202  func NewSMTPClient(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
   203  	return NewSMTPClientAdvanced(
   204  		ctx,
   205  		conn,
   206  		config,
   207  	)
   208  }
   209  
   210  func TestConnection(config *SMTPConfig) error {
   211  	if !config.SendEmailNotifications {
   212  		return errors.New("SendEmailNotifications is not true")
   213  	}
   214  
   215  	conn, err := ConnectToSMTPServer(config)
   216  	if err != nil {
   217  		return errors.Wrap(err, "unable to connect")
   218  	}
   219  	defer conn.Close()
   220  
   221  	sec := config.ServerTimeout
   222  
   223  	ctx := context.Background()
   224  	ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
   225  	defer cancel()
   226  
   227  	c, err := NewSMTPClient(ctx, conn, config)
   228  	if err != nil {
   229  		return errors.Wrap(err, "unable to connect")
   230  	}
   231  	c.Close()
   232  	c.Quit()
   233  
   234  	return nil
   235  }
   236  
   237  func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *SMTPConfig, enableComplianceFeatures bool, ccMail string) error {
   238  	fromMail := mail.Address{Name: config.FeedbackName, Address: config.FeedbackEmail}
   239  	replyTo := mail.Address{Name: config.FeedbackName, Address: config.ReplyToAddress}
   240  
   241  	mail := mailData{
   242  		mimeTo:        to,
   243  		smtpTo:        to,
   244  		from:          fromMail,
   245  		cc:            ccMail,
   246  		replyTo:       replyTo,
   247  		subject:       subject,
   248  		htmlBody:      htmlBody,
   249  		embeddedFiles: embeddedFiles,
   250  	}
   251  
   252  	return sendMailUsingConfigAdvanced(mail, config, enableComplianceFeatures)
   253  }
   254  
   255  func SendMailUsingConfig(to, subject, htmlBody string, config *SMTPConfig, enableComplianceFeatures bool, ccMail string) error {
   256  	return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, ccMail)
   257  }
   258  
   259  // allows for sending an email with differing MIME/SMTP recipients
   260  func sendMailUsingConfigAdvanced(mail mailData, config *SMTPConfig, enableComplianceFeatures bool) error {
   261  	if config.Server == "" {
   262  		return nil
   263  	}
   264  
   265  	conn, err := ConnectToSMTPServer(config)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	defer conn.Close()
   270  
   271  	sec := config.ServerTimeout
   272  
   273  	ctx := context.Background()
   274  	ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
   275  	defer cancel()
   276  
   277  	c, err := NewSMTPClient(ctx, conn, config)
   278  	if err != nil {
   279  		return err
   280  	}
   281  	defer c.Quit()
   282  	defer c.Close()
   283  
   284  	return SendMail(c, mail, time.Now())
   285  }
   286  
   287  func SendMail(c smtpClient, mail mailData, date time.Time) error {
   288  	mlog.Debug("sending mail", mlog.String("to", mail.smtpTo), mlog.String("subject", mail.subject))
   289  
   290  	htmlMessage := "\r\n<html><body>" + mail.htmlBody + "</body></html>"
   291  
   292  	txtBody, err := html2text.FromString(mail.htmlBody)
   293  	if err != nil {
   294  		mlog.Warn("Unable to convert html body to text", mlog.Err(err))
   295  		txtBody = ""
   296  	}
   297  
   298  	headers := map[string][]string{
   299  		"From":                      {mail.from.String()},
   300  		"To":                        {mail.mimeTo},
   301  		"Subject":                   {encodeRFC2047Word(mail.subject)},
   302  		"Content-Transfer-Encoding": {"8bit"},
   303  		"Auto-Submitted":            {"auto-generated"},
   304  		"Precedence":                {"bulk"},
   305  	}
   306  
   307  	if mail.replyTo.Address != "" {
   308  		headers["Reply-To"] = []string{mail.replyTo.String()}
   309  	}
   310  
   311  	if mail.cc != "" {
   312  		headers["CC"] = []string{mail.cc}
   313  	}
   314  
   315  	for k, v := range mail.mimeHeaders {
   316  		headers[k] = []string{encodeRFC2047Word(v)}
   317  	}
   318  
   319  	m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
   320  	m.SetHeaders(headers)
   321  	m.SetDateHeader("Date", date)
   322  	m.SetBody("text/plain", txtBody)
   323  	m.AddAlternative("text/html", htmlMessage)
   324  
   325  	for name, reader := range mail.embeddedFiles {
   326  		m.EmbedReader(name, reader)
   327  	}
   328  
   329  	if err = c.Mail(mail.from.Address); err != nil {
   330  		return errors.Wrap(err, "failed to set the from address")
   331  	}
   332  
   333  	if err = c.Rcpt(mail.smtpTo); err != nil {
   334  		return errors.Wrap(err, "failed to set the to address")
   335  	}
   336  
   337  	w, err := c.Data()
   338  	if err != nil {
   339  		return errors.Wrap(err, "failed to add email message data")
   340  	}
   341  
   342  	_, err = m.WriteTo(w)
   343  	if err != nil {
   344  		return errors.Wrap(err, "failed to write the email message")
   345  	}
   346  	err = w.Close()
   347  	if err != nil {
   348  		return errors.Wrap(err, "failed to close connection to the SMTP server")
   349  	}
   350  
   351  	return nil
   352  }