// Copyright 2014 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package mailer import ( "crypto/tls" "fmt" "net" "net/mail" "net/smtp" "os" "strings" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) type loginAuth struct { username, password string } // SMTP AUTH LOGIN Auth Handler func LoginAuth(username, password string) smtp.Auth { return &loginAuth{username, password} } func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { return "LOGIN", []byte{}, nil } func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { switch string(fromServer) { case "Username:": return []byte(a.username), nil case "Password:": return []byte(a.password), nil default: return nil, fmt.Errorf("unknwon fromServer: %s", string(fromServer)) } } return nil, nil } type Message struct { To []string From string Subject string ReplyTo string Body string Type string Massive bool Info string } // create mail content func (m Message) Content() string { // set mail type contentType := "text/plain; charset=UTF-8" if m.Type == "html" { contentType = "text/html; charset=UTF-8" } // create mail content content := "From: " + m.From + "\r\nReply-To: " + m.ReplyTo + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body return content } var mailQueue chan *Message func NewContext() { if setting.MailService == nil { return } mailQueue = make(chan *Message, setting.MailService.QueueLength) go processMailQueue() } func processMailQueue() { for { select { case msg := <-mailQueue: num, err := Send(msg) tos := strings.Join(msg.To, ", ") info := "" if err != nil { if len(msg.Info) > 0 { info = ", info: " + msg.Info } log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err)) } else { log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info)) } } } } // sendMail allows mail with self-signed certificates. func sendMail(settings *setting.Mailer, recipients []string, msgContent []byte) error { host, port, err := net.SplitHostPort(settings.Host) if err != nil { return err } tlsconfig := &tls.Config{ InsecureSkipVerify: settings.SkipVerify, ServerName: host, } if settings.UseCertificate { cert, err := tls.LoadX509KeyPair(settings.CertFile, settings.KeyFile) if err != nil { return err } tlsconfig.Certificates = []tls.Certificate{cert} } conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) if err != nil { return err } defer conn.Close() isSecureConn := false // Start TLS directly if the port ends with 465 (SMTPS protocol) if strings.HasSuffix(port, "465") { conn = tls.Client(conn, tlsconfig) isSecureConn = true } client, err := smtp.NewClient(conn, host) if err != nil { return err } if !setting.MailService.DisableHelo { hostname := setting.MailService.HeloHostname if len(hostname) == 0 { hostname, err = os.Hostname() if err != nil { return err } } if err = client.Hello(hostname); err != nil { return err } } // If not using SMTPS, alway use STARTTLS if available hasStartTLS, _ := client.Extension("STARTTLS") if !isSecureConn && hasStartTLS { if err = client.StartTLS(tlsconfig); err != nil { return err } } canAuth, options := client.Extension("AUTH") if canAuth && len(settings.User) > 0 { var auth smtp.Auth if strings.Contains(options, "CRAM-MD5") { auth = smtp.CRAMMD5Auth(settings.User, settings.Passwd) } else if strings.Contains(options, "PLAIN") { auth = smtp.PlainAuth("", settings.User, settings.Passwd, host) } else if strings.Contains(options, "LOGIN") { // Patch for AUTH LOGIN auth = LoginAuth(settings.User, settings.Passwd) } if auth != nil { if err = client.Auth(auth); err != nil { return err } } } if fromAddress, err := mail.ParseAddress(settings.From); err != nil { return err } else { if err = client.Mail(fromAddress.Address); err != nil { return err } } for _, rec := range recipients { if err = client.Rcpt(rec); err != nil { return err } } w, err := client.Data() if err != nil { return err } if _, err = w.Write([]byte(msgContent)); err != nil { return err } if err = w.Close(); err != nil { return err } return client.Quit() } // Direct Send mail message func Send(msg *Message) (int, error) { log.Trace("Sending mails to: %s", strings.Join(msg.To, ", ")) // get message body content := msg.Content() if len(msg.To) == 0 { return 0, fmt.Errorf("empty receive emails") } else if len(msg.Body) == 0 { return 0, fmt.Errorf("empty email body") } if msg.Massive { // send mail to multiple emails one by one num := 0 for _, to := range msg.To { body := []byte("To: " + to + "\r\n" + content) err := sendMail(setting.MailService, []string{to}, body) if err != nil { return num, err } num++ } return num, nil } else { body := []byte("To: " + strings.Join(msg.To, ",") + "\r\n" + content) // send to multiple emails in one message err := sendMail(setting.MailService, msg.To, body) if err != nil { return 0, err } else { return 1, nil } } } // Async Send mail message func SendAsync(msg *Message) { go func() { mailQueue <- msg }() } // Create html mail message func NewHtmlMessage(To []string, From, Subject, Body string) Message { return Message{ To: To, From: setting.MailService.From, ReplyTo: From, Subject: Subject, Body: Body, Type: "html", } }