github.com/xzl8028/xenia-server@v0.0.0-20190809101854-18450a97da63/services/mailservice/mail.go (about)

     1  // Copyright (c) 2015-present Xenia, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package mailservice
     5  
     6  import (
     7  	"crypto/tls"
     8  	"errors"
     9  	"fmt"
    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/xzl8028/xenia-server/mlog"
    23  	"github.com/xzl8028/xenia-server/model"
    24  	"github.com/xzl8028/xenia-server/services/filesstore"
    25  	"github.com/xzl8028/xenia-server/utils"
    26  )
    27  
    28  // smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
    29  //
    30  type smtpClient interface {
    31  	Mail(string) error
    32  	Rcpt(string) error
    33  	Data() (io.WriteCloser, error)
    34  }
    35  
    36  func encodeRFC2047Word(s string) string {
    37  	return mime.BEncoding.Encode("utf-8", s)
    38  }
    39  
    40  type SmtpConnectionInfo struct {
    41  	SmtpUsername         string
    42  	SmtpPassword         string
    43  	SmtpServerName       string
    44  	SmtpServerHost       string
    45  	SmtpPort             string
    46  	SkipCertVerification bool
    47  	ConnectionSecurity   string
    48  	Auth                 bool
    49  }
    50  
    51  type authChooser struct {
    52  	smtp.Auth
    53  	connectionInfo *SmtpConnectionInfo
    54  }
    55  
    56  func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
    57  	smtpAddress := a.connectionInfo.SmtpServerName + ":" + a.connectionInfo.SmtpPort
    58  	a.Auth = LoginAuth(a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, smtpAddress)
    59  	for _, method := range server.Auth {
    60  		if method == "PLAIN" {
    61  			a.Auth = smtp.PlainAuth("", a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, a.connectionInfo.SmtpServerName+":"+a.connectionInfo.SmtpPort)
    62  			break
    63  		}
    64  	}
    65  	return a.Auth.Start(server)
    66  }
    67  
    68  type loginAuth struct {
    69  	username, password, host string
    70  }
    71  
    72  func LoginAuth(username, password, host string) smtp.Auth {
    73  	return &loginAuth{username, password, host}
    74  }
    75  
    76  func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
    77  	if !server.TLS {
    78  		return "", nil, errors.New("unencrypted connection")
    79  	}
    80  
    81  	if server.Name != a.host {
    82  		return "", nil, errors.New("wrong host name")
    83  	}
    84  
    85  	return "LOGIN", []byte{}, nil
    86  }
    87  
    88  func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
    89  	if more {
    90  		switch string(fromServer) {
    91  		case "Username:":
    92  			return []byte(a.username), nil
    93  		case "Password:":
    94  			return []byte(a.password), nil
    95  		default:
    96  			return nil, errors.New("Unknown fromServer")
    97  		}
    98  	}
    99  	return nil, nil
   100  }
   101  
   102  func ConnectToSMTPServerAdvanced(connectionInfo *SmtpConnectionInfo) (net.Conn, *model.AppError) {
   103  	var conn net.Conn
   104  	var err error
   105  
   106  	smtpAddress := connectionInfo.SmtpServerHost + ":" + connectionInfo.SmtpPort
   107  	if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_TLS {
   108  		tlsconfig := &tls.Config{
   109  			InsecureSkipVerify: connectionInfo.SkipCertVerification,
   110  			ServerName:         connectionInfo.SmtpServerName,
   111  		}
   112  
   113  		conn, err = tls.Dial("tcp", smtpAddress, tlsconfig)
   114  		if err != nil {
   115  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
   116  		}
   117  	} else {
   118  		conn, err = net.Dial("tcp", smtpAddress)
   119  		if err != nil {
   120  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error(), http.StatusInternalServerError)
   121  		}
   122  	}
   123  
   124  	return conn, nil
   125  }
   126  
   127  func ConnectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
   128  	return ConnectToSMTPServerAdvanced(
   129  		&SmtpConnectionInfo{
   130  			ConnectionSecurity:   *config.EmailSettings.ConnectionSecurity,
   131  			SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
   132  			SmtpServerName:       *config.EmailSettings.SMTPServer,
   133  			SmtpServerHost:       *config.EmailSettings.SMTPServer,
   134  			SmtpPort:             *config.EmailSettings.SMTPPort,
   135  		},
   136  	)
   137  }
   138  
   139  func NewSMTPClientAdvanced(conn net.Conn, hostname string, connectionInfo *SmtpConnectionInfo) (*smtp.Client, *model.AppError) {
   140  	c, err := smtp.NewClient(conn, connectionInfo.SmtpServerName+":"+connectionInfo.SmtpPort)
   141  	if err != nil {
   142  		mlog.Error(fmt.Sprintf("Failed to open a connection to SMTP server %v", err))
   143  		return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
   144  	}
   145  
   146  	if hostname != "" {
   147  		err = c.Hello(hostname)
   148  		if err != nil {
   149  			mlog.Error(fmt.Sprintf("Failed to to set the HELO to SMTP server %v", err))
   150  			return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.helo.app_error", nil, err.Error(), http.StatusInternalServerError)
   151  		}
   152  	}
   153  
   154  	if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
   155  		tlsconfig := &tls.Config{
   156  			InsecureSkipVerify: connectionInfo.SkipCertVerification,
   157  			ServerName:         connectionInfo.SmtpServerName,
   158  		}
   159  		c.StartTLS(tlsconfig)
   160  	}
   161  
   162  	if connectionInfo.Auth {
   163  		if err = c.Auth(&authChooser{connectionInfo: connectionInfo}); err != nil {
   164  			return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError)
   165  		}
   166  	}
   167  	return c, nil
   168  }
   169  
   170  func NewSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
   171  	return NewSMTPClientAdvanced(
   172  		conn,
   173  		utils.GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL),
   174  		&SmtpConnectionInfo{
   175  			ConnectionSecurity:   *config.EmailSettings.ConnectionSecurity,
   176  			SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
   177  			SmtpServerName:       *config.EmailSettings.SMTPServer,
   178  			SmtpServerHost:       *config.EmailSettings.SMTPServer,
   179  			SmtpPort:             *config.EmailSettings.SMTPPort,
   180  			Auth:                 *config.EmailSettings.EnableSMTPAuth,
   181  			SmtpUsername:         *config.EmailSettings.SMTPUsername,
   182  			SmtpPassword:         *config.EmailSettings.SMTPPassword,
   183  		},
   184  	)
   185  }
   186  
   187  func TestConnection(config *model.Config) {
   188  	if !*config.EmailSettings.SendEmailNotifications {
   189  		return
   190  	}
   191  
   192  	conn, err1 := ConnectToSMTPServer(config)
   193  	if err1 != nil {
   194  		mlog.Error(fmt.Sprintf("SMTP server settings do not appear to be configured properly err=%v details=%v", utils.T(err1.Message), err1.DetailedError))
   195  		return
   196  	}
   197  	defer conn.Close()
   198  
   199  	c, err2 := NewSMTPClient(conn, config)
   200  	if err2 != nil {
   201  		mlog.Error(fmt.Sprintf("SMTP server settings do not appear to be configured properly err=%v details=%v", utils.T(err2.Message), err2.DetailedError))
   202  		return
   203  	}
   204  	defer c.Quit()
   205  	defer c.Close()
   206  }
   207  
   208  func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
   209  	fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail}
   210  	replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress}
   211  
   212  	return SendMailUsingConfigAdvanced(to, to, fromMail, replyTo, subject, htmlBody, nil, nil, config, enableComplianceFeatures)
   213  }
   214  
   215  // allows for sending an email with attachments and differing MIME/SMTP recipients
   216  func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
   217  	if !*config.EmailSettings.SendEmailNotifications || len(*config.EmailSettings.SMTPServer) == 0 {
   218  		return nil
   219  	}
   220  
   221  	conn, err := ConnectToSMTPServer(config)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	defer conn.Close()
   226  
   227  	c, err := NewSMTPClient(conn, config)
   228  	if err != nil {
   229  		return err
   230  	}
   231  	defer c.Quit()
   232  	defer c.Close()
   233  
   234  	fileBackend, err := filesstore.NewFileBackend(&config.FileSettings, enableComplianceFeatures)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	return SendMail(c, mimeTo, smtpTo, from, replyTo, subject, htmlBody, attachments, mimeHeaders, fileBackend, time.Now())
   240  }
   241  
   242  func SendMail(c smtpClient, mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend filesstore.FileBackend, date time.Time) *model.AppError {
   243  	mlog.Debug(fmt.Sprintf("sending mail to %v with subject of '%v'", smtpTo, subject))
   244  
   245  	htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>"
   246  
   247  	txtBody, err := html2text.FromString(htmlBody)
   248  	if err != nil {
   249  		mlog.Warn(fmt.Sprint(err))
   250  		txtBody = ""
   251  	}
   252  
   253  	headers := map[string][]string{
   254  		"From":                      {from.String()},
   255  		"To":                        {mimeTo},
   256  		"Subject":                   {encodeRFC2047Word(subject)},
   257  		"Content-Transfer-Encoding": {"8bit"},
   258  		"Auto-Submitted":            {"auto-generated"},
   259  		"Precedence":                {"bulk"},
   260  	}
   261  
   262  	if len(replyTo.Address) > 0 {
   263  		headers["Reply-To"] = []string{replyTo.String()}
   264  	}
   265  
   266  	for k, v := range mimeHeaders {
   267  		headers[k] = []string{encodeRFC2047Word(v)}
   268  	}
   269  
   270  	m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
   271  	m.SetHeaders(headers)
   272  	m.SetDateHeader("Date", date)
   273  	m.SetBody("text/plain", txtBody)
   274  	m.AddAlternative("text/html", htmlMessage)
   275  
   276  	for _, fileInfo := range attachments {
   277  		bytes, err := fileBackend.ReadFile(fileInfo.Path)
   278  		if err != nil {
   279  			return err
   280  		}
   281  
   282  		m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
   283  			if _, err := writer.Write(bytes); err != nil {
   284  				return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError)
   285  			}
   286  			return nil
   287  		}))
   288  	}
   289  
   290  	if err = c.Mail(from.Address); err != nil {
   291  		return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError)
   292  	}
   293  
   294  	if err = c.Rcpt(smtpTo); err != nil {
   295  		return model.NewAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error(), http.StatusInternalServerError)
   296  	}
   297  
   298  	w, err := c.Data()
   299  	if err != nil {
   300  		return model.NewAppError("SendMail", "utils.mail.send_mail.msg_data.app_error", nil, err.Error(), http.StatusInternalServerError)
   301  	}
   302  
   303  	_, err = m.WriteTo(w)
   304  	if err != nil {
   305  		return model.NewAppError("SendMail", "utils.mail.send_mail.msg.app_error", nil, err.Error(), http.StatusInternalServerError)
   306  	}
   307  	err = w.Close()
   308  	if err != nil {
   309  		return model.NewAppError("SendMail", "utils.mail.send_mail.close.app_error", nil, err.Error(), http.StatusInternalServerError)
   310  	}
   311  
   312  	return nil
   313  }