diff --git a/AGENTS.md b/AGENTS.md index 3815ae65f..fe2c3b8f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,5 +28,5 @@ This applies to all texts, including but not limited to UI, documentation, code ## Source code control - When pushing changes to a pull request from a fork, use SSH address and do not add remote. -- Never automatically executes commands that touches Git history even if the session does not require approvals, including but not limited to `rebase`, `commit`, `push`, `pull`, `reset`, `amend`. Exceptions are only allowed case-by-case. -- Do not amend commits unless being explicitly asked to do so. +- Never commit on the `main` branch directly unless being explicitly asked to do so. A single ask only grants a single commit action on the `main` branch. +- Never amend commits unless being explicitly asked to do so. diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8f98a7c..26ba7dd03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to Gogs are documented in this file. ### Removed - The `gogs cert` subcommand. [#8153](https://github.com/gogs/gogs/pull/8153) +- The `[email] DISABLE_HELO` configuration option. HELO/EHLO is now always sent during SMTP handshake. [#8164](https://github.com/gogs/gogs/pull/8164) ## 0.14.1 diff --git a/conf/app.ini b/conf/app.ini index d73885a10..326e8844e 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -197,8 +197,6 @@ USER = noreply@gogs.localhost ; The login password. PASSWORD = -; Whether to disable HELO operation when the hostname is different. -DISABLE_HELO = ; The custom hostname for HELO operation, default is from system. HELO_HOSTNAME = diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index 2c7b3d4a6..9edfa5b5c 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -1262,7 +1262,6 @@ config.email.subject_prefix = Subject prefix config.email.host = Host config.email.from = From config.email.user = User -config.email.disable_helo = Disable HELO config.email.helo_hostname = HELO hostname config.email.skip_verify = Skip certificate verify config.email.use_certificate = Use custom certificate diff --git a/go.mod b/go.mod index 6e55803f2..1dd32c34a 100644 --- a/go.mod +++ b/go.mod @@ -43,11 +43,11 @@ require ( github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6 github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e github.com/urfave/cli/v3 v3.6.2 + github.com/wneessen/go-mail v0.7.2 golang.org/x/crypto v0.47.0 golang.org/x/image v0.35.0 golang.org/x/net v0.48.0 golang.org/x/text v0.33.0 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/macaron.v1 v1.5.1 gorm.io/driver/mysql v1.5.2 @@ -132,7 +132,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1147259d5..4182646c8 100644 --- a/go.sum +++ b/go.sum @@ -433,6 +433,8 @@ github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e h1:Qf3QQl/zmEbWD github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e/go.mod h1:TBwoao3Q4Eb/cp+dHbXDfRTrZSsj/k7kLr2j1oWRWC0= github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= +github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -589,8 +591,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU= gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -599,8 +599,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/internal/conf/static.go b/internal/conf/static.go index 2bc34bd72..9929003b9 100644 --- a/internal/conf/static.go +++ b/internal/conf/static.go @@ -56,7 +56,6 @@ var ( User string Password string - DisableHELO bool `ini:"DISABLE_HELO"` HELOHostname string `ini:"HELO_HOSTNAME"` SkipVerify bool diff --git a/internal/conf/testdata/TestInit.golden.ini b/internal/conf/testdata/TestInit.golden.ini index 98907dbc5..29554da38 100644 --- a/internal/conf/testdata/TestInit.golden.ini +++ b/internal/conf/testdata/TestInit.golden.ini @@ -88,7 +88,6 @@ HOST=smtp.mailgun.org:587 FROM=noreply@gogs.localhost USER=noreply@gogs.localhost PASSWORD=87654321 -DISABLE_HELO=false HELO_HOSTNAME= SKIP_VERIFY=false USE_CERTIFICATE=false diff --git a/internal/database/issue_mail.go b/internal/database/issue_mail.go index 411c678cc..59d6aa1e6 100644 --- a/internal/database/issue_mail.go +++ b/internal/database/issue_mail.go @@ -151,7 +151,9 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) names = append(names, issue.Assignee.Name) } } - email.SendIssueCommentMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), tos) + if err = email.SendIssueCommentMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), tos); err != nil { + return errors.Wrap(err, "send issue comment mail") + } // Mail mentioned people and exclude watchers. names = append(names, doer.Name) @@ -168,7 +170,9 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) if err != nil { return errors.Wrap(err, "get mailable emails by usernames") } - email.SendIssueMentionMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), tos) + if err = email.SendIssueMentionMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), tos); err != nil { + return errors.Wrap(err, "send issue mention mail") + } return nil } diff --git a/internal/email/email.go b/internal/email/email.go index 319a7ae6f..b867dc082 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -3,13 +3,13 @@ package email import ( "fmt" "html/template" + "net/mail" "path/filepath" "sync" "time" - "gopkg.in/gomail.v2" + "github.com/cockroachdb/errors" "gopkg.in/macaron.v1" - log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/markup" @@ -72,7 +72,11 @@ func render(tpl string, data map[string]any) (string, error) { } func SendTestMail(email string) error { - return gomail.Send(&Sender{}, NewMessage([]string{email}, "Gogs Test Email", "Hello 👋, greeting from Gogs!").Message) + msg, err := newMessage([]string{email}, "Gogs Test Email", "Hello 👋, greeting from Gogs!") + if err != nil { + return errors.Wrap(err, "new message") + } + return sendMessage(msg) } /* @@ -98,7 +102,7 @@ type Issue interface { HTMLURL() string } -func SendUserMail(_ *macaron.Context, u User, tpl, code, subject, info string) { +func SendUserMail(_ *macaron.Context, u User, tpl, code, subject, info string) error { data := map[string]any{ "Username": u.DisplayName(), "ActiveCodeLives": conf.Auth.ActivateCodeLives / 60, @@ -107,26 +111,28 @@ func SendUserMail(_ *macaron.Context, u User, tpl, code, subject, info string) { } body, err := render(tpl, data) if err != nil { - log.Error("render: %v", err) - return + return errors.Wrap(err, "render") } - msg := NewMessage([]string{u.Email()}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, %s", u.ID(), info) + msg, err := newMessage([]string{u.Email()}, subject, body) + if err != nil { + return errors.Wrap(err, "new message") + } + msg.info = fmt.Sprintf("UID: %d, %s", u.ID(), info) - Send(msg) + send(msg) + return nil } -func SendActivateAccountMail(c *macaron.Context, u User) { - SendUserMail(c, u, tmplAuthActivate, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.activate_account"), "activate account") +func SendActivateAccountMail(c *macaron.Context, u User) error { + return SendUserMail(c, u, tmplAuthActivate, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.activate_account"), "activate account") } -func SendResetPasswordMail(c *macaron.Context, u User) { - SendUserMail(c, u, tmplAuthResetPassword, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.reset_password"), "reset password") +func SendResetPasswordMail(c *macaron.Context, u User) error { + return SendUserMail(c, u, tmplAuthResetPassword, u.GenerateEmailActivateCode(u.Email()), c.Tr("mail.reset_password"), "reset password") } -// SendActivateAccountMail sends confirmation email. -func SendActivateEmailMail(c *macaron.Context, u User, email string) { +func SendActivateEmailMail(c *macaron.Context, u User, email string) error { data := map[string]any{ "Username": u.DisplayName(), "ActiveCodeLives": conf.Auth.ActivateCodeLives / 60, @@ -135,35 +141,39 @@ func SendActivateEmailMail(c *macaron.Context, u User, email string) { } body, err := render(tmplAuthActivateEmail, data) if err != nil { - log.Error("HTMLString: %v", err) - return + return errors.Wrap(err, "render") } - msg := NewMessage([]string{email}, c.Tr("mail.activate_email"), body) - msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID()) + msg, err := newMessage([]string{email}, c.Tr("mail.activate_email"), body) + if err != nil { + return errors.Wrap(err, "new message") + } + msg.info = fmt.Sprintf("UID: %d, activate email", u.ID()) - Send(msg) + send(msg) + return nil } -// SendRegisterNotifyMail triggers a notify e-mail by admin created a account. -func SendRegisterNotifyMail(c *macaron.Context, u User) { +func SendRegisterNotifyMail(c *macaron.Context, u User) error { data := map[string]any{ "Username": u.DisplayName(), } body, err := render(tmplAuthRegisterNotify, data) if err != nil { - log.Error("HTMLString: %v", err) - return + return errors.Wrap(err, "render") } - msg := NewMessage([]string{u.Email()}, c.Tr("mail.register_notify"), body) - msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID()) + msg, err := newMessage([]string{u.Email()}, c.Tr("mail.register_notify"), body) + if err != nil { + return errors.Wrap(err, "new message") + } + msg.info = fmt.Sprintf("UID: %d, registration notify", u.ID()) - Send(msg) + send(msg) + return nil } -// SendCollaboratorMail sends mail notification to new collaborator. -func SendCollaboratorMail(u, doer User, repo Repository) { +func SendCollaboratorMail(u, doer User, repo Repository) error { subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repo.FullName()) data := map[string]any{ @@ -173,14 +183,17 @@ func SendCollaboratorMail(u, doer User, repo Repository) { } body, err := render(tmplNotifyCollaborator, data) if err != nil { - log.Error("HTMLString: %v", err) - return + return errors.Wrap(err, "render") } - msg := NewMessage([]string{u.Email()}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID()) + msg, err := newMessage([]string{u.Email()}, subject, body) + if err != nil { + return errors.Wrap(err, "new message") + } + msg.info = fmt.Sprintf("UID: %d, add collaborator", u.ID()) - Send(msg) + send(msg) + return nil } func composeTplData(subject, body, link string) map[string]any { @@ -191,34 +204,47 @@ func composeTplData(subject, body, link string) map[string]any { return data } -func composeIssueMessage(issue Issue, repo Repository, doer User, tplName string, tos []string, info string) *Message { +func composeIssueMessage(issue Issue, repo Repository, doer User, tplName string, tos []string, info string) (*message, error) { subject := issue.MailSubject() body := string(markup.Markdown([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas())) data := composeTplData(subject, body, issue.HTMLURL()) data["Doer"] = doer content, err := render(tplName, data) if err != nil { - log.Error("HTMLString (%s): %v", tplName, err) + return nil, errors.Wrapf(err, "render %q", tplName) } - from := gomail.NewMessage().FormatAddress(conf.Email.FromEmail, doer.DisplayName()) - msg := NewMessageFrom(tos, from, subject, content) - msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info) - return msg + from := (&mail.Address{Name: doer.DisplayName(), Address: conf.Email.FromEmail}).String() + msg, err := newMessageFrom(tos, from, subject, content) + if err != nil { + return nil, errors.Wrap(err, "new message") + } + msg.info = fmt.Sprintf("Subject: %s, %s", subject, info) + return msg, nil } // SendIssueCommentMail composes and sends issue comment emails to target receivers. -func SendIssueCommentMail(issue Issue, repo Repository, doer User, tos []string) { +func SendIssueCommentMail(issue Issue, repo Repository, doer User, tos []string) error { if len(tos) == 0 { - return + return nil } - Send(composeIssueMessage(issue, repo, doer, tmplIssueComment, tos, "issue comment")) + msg, err := composeIssueMessage(issue, repo, doer, tmplIssueComment, tos, "issue comment") + if err != nil { + return errors.Wrap(err, "compose issue message") + } + send(msg) + return nil } // SendIssueMentionMail composes and sends issue mention emails to target receivers. -func SendIssueMentionMail(issue Issue, repo Repository, doer User, tos []string) { +func SendIssueMentionMail(issue Issue, repo Repository, doer User, tos []string) error { if len(tos) == 0 { - return + return nil } - Send(composeIssueMessage(issue, repo, doer, tmplIssueMention, tos, "issue mention")) + msg, err := composeIssueMessage(issue, repo, doer, tmplIssueMention, tos, "issue mention") + if err != nil { + return errors.Wrap(err, "compose issue message") + } + send(msg) + return nil } diff --git a/internal/email/message.go b/internal/email/message.go index f84086e95..392eb8cf2 100644 --- a/internal/email/message.go +++ b/internal/email/message.go @@ -2,214 +2,134 @@ package email import ( "crypto/tls" - "io" "net" - "net/smtp" - "os" + "strconv" "strings" - "time" "github.com/cockroachdb/errors" "github.com/inbucket/html2text" - "gopkg.in/gomail.v2" + gomail "github.com/wneessen/go-mail" log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/conf" ) -type Message struct { - Info string // Message information for log purpose. - *gomail.Message +type message struct { + info string + msg *gomail.Msg confirmChan chan struct{} } -// NewMessageFrom creates new mail message object with custom From header. -func NewMessageFrom(to []string, from, subject, htmlBody string) *Message { +func newMessageFrom(to []string, from, subject, htmlBody string) (*message, error) { log.Trace("NewMessageFrom (htmlBody):\n%s", htmlBody) - msg := gomail.NewMessage() - msg.SetHeader("From", from) - msg.SetHeader("To", to...) - msg.SetHeader("Subject", conf.Email.SubjectPrefix+subject) - msg.SetDateHeader("Date", time.Now()) + m := gomail.NewMsg() + if err := m.From(from); err != nil { + return nil, errors.Wrapf(err, "set From address %q", from) + } + if err := m.To(to...); err != nil { + return nil, errors.Wrap(err, "set To addresses") + } + m.Subject(conf.Email.SubjectPrefix + subject) + m.SetDate() - contentType := "text/html" - body := htmlBody - switchedToPlaintext := false if conf.Email.UsePlainText || conf.Email.AddPlainTextAlt { plainBody, err := html2text.FromString(htmlBody) if err != nil { - log.Error("html2text.FromString: %v", err) + return nil, errors.Wrap(err, "convert HTML to plain text") + } + if conf.Email.UsePlainText { + m.SetBodyString(gomail.TypeTextPlain, plainBody) } else { - contentType = "text/plain" - body = plainBody - switchedToPlaintext = true + m.SetBodyString(gomail.TypeTextPlain, plainBody) + m.AddAlternativeString(gomail.TypeTextHTML, htmlBody) } + } else { + m.SetBodyString(gomail.TypeTextHTML, htmlBody) } - msg.SetBody(contentType, body) - if switchedToPlaintext && conf.Email.AddPlainTextAlt && !conf.Email.UsePlainText { - // The AddAlternative method name is confusing - adding html as an "alternative" will actually cause mail - // clients to show it as first priority, and the text "main body" is the 2nd priority fallback. - // See: https://godoc.org/gopkg.in/gomail.v2#Message.AddAlternative - msg.AddAlternative("text/html", htmlBody) - } - return &Message{ - Message: msg, + + return &message{ + msg: m, confirmChan: make(chan struct{}), - } + }, nil } -// NewMessage creates new mail message object with default From header. -func NewMessage(to []string, subject, body string) *Message { - return NewMessageFrom(to, conf.Email.From, subject, body) +func newMessage(to []string, subject, body string) (*message, error) { + return newMessageFrom(to, conf.Email.From, subject, body) } -type loginAuth struct { - username, password string -} - -// SMTP AUTH LOGIN Auth Handler -func LoginAuth(username, password string) smtp.Auth { - return &loginAuth{username, password} -} - -func (*loginAuth) Start(_ *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, errors.Newf("unknwon fromServer: %s", string(fromServer)) - } - } - return nil, nil -} - -type Sender struct{} - -func (*Sender) Send(from string, to []string, msg io.WriterTo) error { +func newSMTPClient() (*gomail.Client, error) { opts := conf.Email - host, port, err := net.SplitHostPort(opts.Host) + host, portStr, err := net.SplitHostPort(opts.Host) if err != nil { - return err + return nil, err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + + clientOpts := []gomail.Option{ + gomail.WithPort(port), + } + + if port == 465 { + clientOpts = append(clientOpts, gomail.WithSSL()) + } else { + clientOpts = append(clientOpts, gomail.WithTLSPolicy(gomail.TLSOpportunistic)) + } + + if opts.HELOHostname != "" { + clientOpts = append(clientOpts, gomail.WithHELO(opts.HELOHostname)) } tlsconfig := &tls.Config{ InsecureSkipVerify: opts.SkipVerify, ServerName: host, } - if opts.UseCertificate { cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile) if err != nil { - return err + return nil, err } tlsconfig.Certificates = []tls.Certificate{cert} } + clientOpts = append(clientOpts, gomail.WithTLSConfig(tlsconfig)) - conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) + if len(opts.User) > 0 { + clientOpts = append(clientOpts, + gomail.WithSMTPAuth(gomail.SMTPAuthAutoDiscover), + gomail.WithUsername(opts.User), + gomail.WithPassword(opts.Password), + ) + } + + return gomail.NewClient(host, clientOpts...) +} + +func sendMessage(msg *message) error { + client, err := newSMTPClient() 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 errors.Newf("NewClient: %v", err) - } - - if !opts.DisableHELO { - hostname := opts.HELOHostname - if hostname == "" { - hostname, err = os.Hostname() - if err != nil { - return err - } - } - - if err = client.Hello(hostname); err != nil { - return errors.Newf("hello: %v", err) - } - } - - // If not using SMTPS, always use STARTTLS if available - hasStartTLS, _ := client.Extension("STARTTLS") - if !isSecureConn && hasStartTLS { - if err = client.StartTLS(tlsconfig); err != nil { - return errors.Newf("StartTLS: %v", err) - } - } - - canAuth, options := client.Extension("AUTH") - if canAuth && len(opts.User) > 0 { - var auth smtp.Auth - - if strings.Contains(options, "CRAM-MD5") { - auth = smtp.CRAMMD5Auth(opts.User, opts.Password) - } else if strings.Contains(options, "PLAIN") { - auth = smtp.PlainAuth("", opts.User, opts.Password, host) - } else if strings.Contains(options, "LOGIN") { - // Patch for AUTH LOGIN - auth = LoginAuth(opts.User, opts.Password) - } - - if auth != nil { - if err = client.Auth(auth); err != nil { - return errors.Newf("auth: %v", err) - } - } - } - - if err = client.Mail(from); err != nil { - return errors.Newf("mail: %v", err) - } - - for _, rec := range to { - if err = client.Rcpt(rec); err != nil { - return errors.Newf("rcpt: %v", err) - } - } - - w, err := client.Data() - if err != nil { - return errors.Newf("data: %v", err) - } else if _, err = msg.WriteTo(w); err != nil { - return errors.Newf("write to: %v", err) - } else if err = w.Close(); err != nil { - return errors.Newf("close: %v", err) - } - - return client.Quit() + return client.DialAndSend(msg.msg) } func processMailQueue() { - sender := &Sender{} for msg := range mailQueue { - log.Trace("New e-mail sending request %s: %s", msg.GetHeader("To"), msg.Info) - if err := gomail.Send(sender, msg.Message); err != nil { - log.Error("Failed to send emails %s: %s - %v", msg.GetHeader("To"), msg.Info, err) + to := strings.Join(msg.msg.GetToString(), ", ") + log.Trace("New e-mail sending request %s: %s", to, msg.info) + if err := sendMessage(msg); err != nil { + log.Error("Failed to send emails %s: %s - %v", to, msg.info, err) } else { - log.Trace("E-mails sent %s: %s", msg.GetHeader("To"), msg.Info) + log.Trace("E-mails sent %s: %s", to, msg.info) } msg.confirmChan <- struct{}{} } } -var mailQueue chan *Message +var mailQueue chan *message // NewContext initializes settings for mailer. func NewContext() { @@ -220,14 +140,14 @@ func NewContext() { return } - mailQueue = make(chan *Message, 1000) + mailQueue = make(chan *message, 1000) go processMailQueue() } -// Send puts new message object into mail queue. +// send puts new message object into mail queue. // It returns without confirmation (mail processed asynchronously) in normal cases, // but waits/blocks under hook mode to make sure mail has been sent. -func Send(msg *Message) { +func send(msg *message) { if !conf.Email.Enabled { return } diff --git a/internal/route/admin/users.go b/internal/route/admin/users.go index c68861578..077814269 100644 --- a/internal/route/admin/users.go +++ b/internal/route/admin/users.go @@ -106,7 +106,9 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) { // Send email notification. if f.SendNotify && conf.Email.Enabled { - email.SendRegisterNotifyMail(c.Context, database.NewMailerUser(user)) + if err := email.SendRegisterNotifyMail(c.Context, database.NewMailerUser(user)); err != nil { + log.Error("Failed to send register notify mail: %v", err) + } } c.Flash.Success(c.Tr("admin.users.new_success", user.Name)) diff --git a/internal/route/api/v1/admin_user.go b/internal/route/api/v1/admin_user.go index 915c1e80f..3dedc699e 100644 --- a/internal/route/api/v1/admin_user.go +++ b/internal/route/api/v1/admin_user.go @@ -69,7 +69,9 @@ func adminCreateUser(c *context.APIContext, form adminCreateUserRequest) { // Send email notification. if form.SendNotify && conf.Email.Enabled { - email.SendRegisterNotifyMail(c.Context.Context, database.NewMailerUser(u)) + if err := email.SendRegisterNotifyMail(c.Context.Context, database.NewMailerUser(u)); err != nil { + log.Error("Failed to send register notify mail: %v", err) + } } c.JSON(http.StatusCreated, toUser(u)) diff --git a/internal/route/repo/setting.go b/internal/route/repo/setting.go index de8050b49..d6e5f8676 100644 --- a/internal/route/repo/setting.go +++ b/internal/route/repo/setting.go @@ -401,7 +401,9 @@ func SettingsCollaborationPost(c *context.Context) { } if conf.User.EnableEmailNotification { - email.SendCollaboratorMail(database.NewMailerUser(u), database.NewMailerUser(c.User), database.NewMailerRepo(c.Repo.Repository)) + if err := email.SendCollaboratorMail(database.NewMailerUser(u), database.NewMailerUser(c.User), database.NewMailerRepo(c.Repo.Repository)); err != nil { + log.Error("Failed to send collaborator mail: %v", err) + } } c.Flash.Success(c.Tr("repo.settings.add_collaborator_success")) diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go index 026bce215..125b568c1 100644 --- a/internal/route/user/auth.go +++ b/internal/route/user/auth.go @@ -385,7 +385,9 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) { // Send confirmation email. if conf.Auth.RequireEmailConfirmation && user.ID > 1 { - email.SendActivateAccountMail(c.Context, database.NewMailerUser(user)) + if err := email.SendActivateAccountMail(c.Context, database.NewMailerUser(user)); err != nil { + log.Error("Failed to send activate account mail: %v", err) + } c.Data["IsSendRegisterMail"] = true c.Data["Email"] = user.Email c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60 @@ -469,7 +471,9 @@ func Activate(c *context.Context) { c.Data["ResendLimited"] = true } else { c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60 - email.SendActivateAccountMail(c.Context, database.NewMailerUser(c.User)) + if err := email.SendActivateAccountMail(c.Context, database.NewMailerUser(c.User)); err != nil { + log.Error("Failed to send activate account mail: %v", err) + } if err := c.Cache.Put(userutil.MailResendCacheKey(c.User.ID), 1, 180); err != nil { log.Error("Failed to put cache key 'mail resend': %v", err) @@ -579,7 +583,9 @@ func ForgotPasswdPost(c *context.Context) { return } - email.SendResetPasswordMail(c.Context, database.NewMailerUser(u)) + if err = email.SendResetPasswordMail(c.Context, database.NewMailerUser(u)); err != nil { + log.Error("Failed to send reset password mail: %v", err) + } if err = c.Cache.Put(userutil.MailResendCacheKey(u.ID), 1, 180); err != nil { log.Error("Failed to put cache key 'mail resend': %v", err) } diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go index 14bde8d02..8e635490f 100644 --- a/internal/route/user/setting.go +++ b/internal/route/user/setting.go @@ -280,7 +280,9 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) { // Send confirmation email if conf.Auth.RequireEmailConfirmation { - email.SendActivateEmailMail(c.Context, database.NewMailerUser(c.User), f.Email) + if err := email.SendActivateEmailMail(c.Context, database.NewMailerUser(c.User), f.Email); err != nil { + log.Error("Failed to send activate email mail: %v", err) + } if err := c.Cache.Put("MailResendLimit_"+c.User.LowerName, c.User.LowerName, 180); err != nil { log.Error("Set cache 'MailResendLimit' failed: %v", err) diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index a0e10100c..1d9541eae 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -237,8 +237,6 @@
-