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 }