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 }