github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/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  	"errors"
    10  	"io"
    11  	"mime"
    12  	"net"
    13  	"net/mail"
    14  	"net/smtp"
    15  	"time"
    16  
    17  	gomail "gopkg.in/mail.v2"
    18  
    19  	"net/http"
    20  
    21  	"github.com/jaytaylor/html2text"
    22  	"github.com/mattermost/mattermost-server/v5/mlog"
    23  	"github.com/mattermost/mattermost-server/v5/model"
    24  	"github.com/mattermost/mattermost-server/v5/services/filesstore"
    25  	"github.com/mattermost/mattermost-server/v5/utils"
    26  )
    27  
    28  type mailData struct {
    29  	mimeTo        string
    30  	smtpTo        string
    31  	from          mail.Address
    32  	cc            string
    33  	replyTo       mail.Address
    34  	subject       string
    35  	htmlBody      string
    36  	attachments   []*model.FileInfo
    37  	embeddedFiles map[string]io.Reader
    38  	mimeHeaders   map[string]string
    39  }
    40  
    41  // smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
    42  //
    43  type smtpClient interface {
    44  	Mail(string) error
    45  	Rcpt(string) error
    46  	Data() (io.WriteCloser, error)
    47  }
    48  
    49  func encodeRFC2047Word(s string) string {
    50  	return mime.BEncoding.Encode("utf-8", s)
    51  }
    52  
    53  type SmtpConnectionInfo struct {
    54  	SmtpUsername         string
    55  	SmtpPassword         string
    56  	SmtpServerName       string
    57  	SmtpServerHost       string
    58  	SmtpPort             string
    59  	SmtpServerTimeout    int
    60  	SkipCertVerification bool
    61  	ConnectionSecurity   string
    62  	Auth                 bool
    63  }
    64  
    65  type authChooser struct {
    66  	smtp.Auth
    67  	connectionInfo *SmtpConnectionInfo
    68  }
    69  
    70  func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
    71  	smtpAddress := a.connectionInfo.SmtpServerName + ":" + a.connectionInfo.SmtpPort
    72  	a.Auth = LoginAuth(a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, smtpAddress)
    73  	for _, method := range server.Auth {
    74  		if method == "PLAIN" {
    75  			a.Auth = smtp.PlainAuth("", a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, a.connectionInfo.SmtpServerName+":"+a.connectionInfo.SmtpPort)
    76  			break
    77  		}
    78  	}
    79  	return a.Auth.Start(server)
    80  }
    81  
    82  type loginAuth struct {
    83  	username, password, host string
    84  }
    85  
    86  func LoginAuth(username, password, host string) smtp.Auth {
    87  	return &loginAuth{username, password, host}
    88  }
    89  
    90  func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    91  	if !server.TLS {
    92  		return "", nil, errors.New("unencrypted connection")
    93  	}
    94  
    95  	if server.Name != a.host {
    96  		return "", nil, errors.New("wrong host name")
    97  	}
    98  
    99  	return "LOGIN", []byte{}, nil
   100  }
   101  
   102  func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
   103  	if more {
   104  		switch string(fromServer) {
   105  		case "Username:":
   106  			return []byte(a.username), nil
   107  		case "Password:":
   108  			return []byte(a.password), nil
   109  		default:
   110  			return nil, errors.New("Unknown fromServer")
   111  		}
   112  	}
   113  	return nil, nil
   114  }
   115  
   116  func ConnectToSMTPServerAdvanced(connectionInfo *SmtpConnectionInfo) (net.Conn, *model.AppError) {
   117  	var conn net.Conn
   118  	var err error
   119  
   120  	smtpAddress := connectionInfo.SmtpServerHost + ":" + connectionInfo.SmtpPort
   121  	dialer := &net.Dialer{
   122  		Timeout: time.Duration(connectionInfo.SmtpServerTimeout) * time.Second,
   123  	}
   124  
   125  	if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_TLS {
   126  		tlsconfig := &tls.Config{
   127  			InsecureSkipVerify: connectionInfo.SkipCertVerification,
   128  			ServerName:         connectionInfo.SmtpServerName,
   129  		}
   130  
   131  		conn, err = tls.DialWithDialer(dialer, "tcp", smtpAddress, tlsconfig)
   132  		if err != nil {
   133  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
   134  		}
   135  	} else {
   136  		conn, err = dialer.Dial("tcp", smtpAddress)
   137  		if err != nil {
   138  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error(), http.StatusInternalServerError)
   139  		}
   140  	}
   141  
   142  	return conn, nil
   143  }
   144  
   145  func ConnectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
   146  	return ConnectToSMTPServerAdvanced(
   147  		&SmtpConnectionInfo{
   148  			ConnectionSecurity:   *config.EmailSettings.ConnectionSecurity,
   149  			SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
   150  			SmtpServerName:       *config.EmailSettings.SMTPServer,
   151  			SmtpServerHost:       *config.EmailSettings.SMTPServer,
   152  			SmtpPort:             *config.EmailSettings.SMTPPort,
   153  			SmtpServerTimeout:    *config.EmailSettings.SMTPServerTimeout,
   154  		},
   155  	)
   156  }
   157  
   158  func NewSMTPClientAdvanced(ctx context.Context, conn net.Conn, hostname string, connectionInfo *SmtpConnectionInfo) (*smtp.Client, *model.AppError) {
   159  	ctx, cancel := context.WithCancel(ctx)
   160  	defer cancel()
   161  
   162  	var c *smtp.Client
   163  	ec := make(chan error)
   164  	go func() {
   165  		var err error
   166  		c, err = smtp.NewClient(conn, connectionInfo.SmtpServerName+":"+connectionInfo.SmtpPort)
   167  		if err != nil {
   168  			ec <- err
   169  			return
   170  		}
   171  		cancel()
   172  	}()
   173  
   174  	select {
   175  	case <-ctx.Done():
   176  		err := ctx.Err()
   177  		if err != nil && err.Error() != "context canceled" {
   178  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
   179  		}
   180  	case err := <-ec:
   181  		return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
   182  	}
   183  
   184  	if hostname != "" {
   185  		err := c.Hello(hostname)
   186  		if err != nil {
   187  			mlog.Error("Failed to to set the HELO to SMTP server", mlog.Err(err))
   188  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.helo.app_error", nil, err.Error(), http.StatusInternalServerError)
   189  		}
   190  	}
   191  
   192  	if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
   193  		tlsconfig := &tls.Config{
   194  			InsecureSkipVerify: connectionInfo.SkipCertVerification,
   195  			ServerName:         connectionInfo.SmtpServerName,
   196  		}
   197  		c.StartTLS(tlsconfig)
   198  	}
   199  
   200  	if connectionInfo.Auth {
   201  		if err := c.Auth(&authChooser{connectionInfo: connectionInfo}); err != nil {
   202  			return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError)
   203  		}
   204  	}
   205  	return c, nil
   206  }
   207  
   208  func NewSMTPClient(ctx context.Context, conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
   209  	return NewSMTPClientAdvanced(
   210  		ctx,
   211  		conn,
   212  		utils.GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL),
   213  		&SmtpConnectionInfo{
   214  			ConnectionSecurity:   *config.EmailSettings.ConnectionSecurity,
   215  			SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
   216  			SmtpServerName:       *config.EmailSettings.SMTPServer,
   217  			SmtpServerHost:       *config.EmailSettings.SMTPServer,
   218  			SmtpPort:             *config.EmailSettings.SMTPPort,
   219  			SmtpServerTimeout:    *config.EmailSettings.SMTPServerTimeout,
   220  			Auth:                 *config.EmailSettings.EnableSMTPAuth,
   221  			SmtpUsername:         *config.EmailSettings.SMTPUsername,
   222  			SmtpPassword:         *config.EmailSettings.SMTPPassword,
   223  		},
   224  	)
   225  }
   226  
   227  func TestConnection(config *model.Config) *model.AppError {
   228  	if !*config.EmailSettings.SendEmailNotifications {
   229  		return &model.AppError{Message: "SendEmailNotifications is not true"}
   230  	}
   231  
   232  	conn, err := ConnectToSMTPServer(config)
   233  	if err != nil {
   234  		return &model.AppError{Message: "Could not connect to SMTP server, check SMTP server settings.", DetailedError: err.DetailedError}
   235  	}
   236  	defer conn.Close()
   237  
   238  	sec := *config.EmailSettings.SMTPServerTimeout
   239  
   240  	ctx := context.Background()
   241  	ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
   242  	defer cancel()
   243  
   244  	c, err := NewSMTPClient(ctx, conn, config)
   245  	if err != nil {
   246  		return &model.AppError{Message: "Could not connect to SMTP server, check SMTP server settings."}
   247  	}
   248  	c.Close()
   249  	c.Quit()
   250  
   251  	return nil
   252  }
   253  
   254  func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
   255  	fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail}
   256  	replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress}
   257  
   258  	mail := mailData{
   259  		mimeTo:        to,
   260  		smtpTo:        to,
   261  		from:          fromMail,
   262  		cc:            ccMail,
   263  		replyTo:       replyTo,
   264  		subject:       subject,
   265  		htmlBody:      htmlBody,
   266  		embeddedFiles: embeddedFiles,
   267  	}
   268  
   269  	return sendMailUsingConfigAdvanced(mail, config, enableComplianceFeatures)
   270  }
   271  
   272  func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
   273  	return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, ccMail)
   274  }
   275  
   276  // allows for sending an email with attachments and differing MIME/SMTP recipients
   277  func sendMailUsingConfigAdvanced(mail mailData, config *model.Config, enableComplianceFeatures bool) *model.AppError {
   278  	if len(*config.EmailSettings.SMTPServer) == 0 {
   279  		return nil
   280  	}
   281  
   282  	conn, err := ConnectToSMTPServer(config)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	defer conn.Close()
   287  
   288  	sec := *config.EmailSettings.SMTPServerTimeout
   289  
   290  	ctx := context.Background()
   291  	ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
   292  	defer cancel()
   293  
   294  	c, err := NewSMTPClient(ctx, conn, config)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	defer c.Quit()
   299  	defer c.Close()
   300  
   301  	fileBackend, err := filesstore.NewFileBackend(&config.FileSettings, enableComplianceFeatures)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	return SendMail(c, mail, fileBackend, time.Now())
   307  }
   308  
   309  func SendMail(c smtpClient, mail mailData, fileBackend filesstore.FileBackend, date time.Time) *model.AppError {
   310  	mlog.Debug("sending mail", mlog.String("to", mail.smtpTo), mlog.String("subject", mail.subject))
   311  
   312  	htmlMessage := "\r\n<html><body>" + mail.htmlBody + "</body></html>"
   313  
   314  	txtBody, err := html2text.FromString(mail.htmlBody)
   315  	if err != nil {
   316  		mlog.Warn("Unable to convert html body to text", mlog.Err(err))
   317  		txtBody = ""
   318  	}
   319  
   320  	headers := map[string][]string{
   321  		"From":                      {mail.from.String()},
   322  		"To":                        {mail.mimeTo},
   323  		"Subject":                   {encodeRFC2047Word(mail.subject)},
   324  		"Content-Transfer-Encoding": {"8bit"},
   325  		"Auto-Submitted":            {"auto-generated"},
   326  		"Precedence":                {"bulk"},
   327  	}
   328  
   329  	if len(mail.replyTo.Address) > 0 {
   330  		headers["Reply-To"] = []string{mail.replyTo.String()}
   331  	}
   332  
   333  	if len(mail.cc) > 0 {
   334  		headers["CC"] = []string{mail.cc}
   335  	}
   336  
   337  	for k, v := range mail.mimeHeaders {
   338  		headers[k] = []string{encodeRFC2047Word(v)}
   339  	}
   340  
   341  	m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
   342  	m.SetHeaders(headers)
   343  	m.SetDateHeader("Date", date)
   344  	m.SetBody("text/plain", txtBody)
   345  	m.AddAlternative("text/html", htmlMessage)
   346  
   347  	for name, reader := range mail.embeddedFiles {
   348  		m.EmbedReader(name, reader)
   349  	}
   350  
   351  	for _, fileInfo := range mail.attachments {
   352  		bytes, err := fileBackend.ReadFile(fileInfo.Path)
   353  		if err != nil {
   354  			return err
   355  		}
   356  
   357  		m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
   358  			if _, err := writer.Write(bytes); err != nil {
   359  				return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError)
   360  			}
   361  			return nil
   362  		}))
   363  	}
   364  
   365  	if err = c.Mail(mail.from.Address); err != nil {
   366  		return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError)
   367  	}
   368  
   369  	if err = c.Rcpt(mail.smtpTo); err != nil {
   370  		return model.NewAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error(), http.StatusInternalServerError)
   371  	}
   372  
   373  	w, err := c.Data()
   374  	if err != nil {
   375  		return model.NewAppError("SendMail", "utils.mail.send_mail.msg_data.app_error", nil, err.Error(), http.StatusInternalServerError)
   376  	}
   377  
   378  	_, err = m.WriteTo(w)
   379  	if err != nil {
   380  		return model.NewAppError("SendMail", "utils.mail.send_mail.msg.app_error", nil, err.Error(), http.StatusInternalServerError)
   381  	}
   382  	err = w.Close()
   383  	if err != nil {
   384  		return model.NewAppError("SendMail", "utils.mail.send_mail.close.app_error", nil, err.Error(), http.StatusInternalServerError)
   385  	}
   386  
   387  	return nil
   388  }