1
0
Просмотр исходного кода

email: replace gomail with go-mail (#8164)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ᴊᴏᴇ ᴄʜᴇɴ 3 дней назад
Родитель
Сommit
94d6e53dc2

+ 2 - 2
AGENTS.md

@@ -28,5 +28,5 @@ This applies to all texts, including but not limited to UI, documentation, code
 ## Source code control
 ## Source code control
 
 
 - When pushing changes to a pull request from a fork, use SSH address and do not add remote.
 - 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.

+ 1 - 0
CHANGELOG.md

@@ -11,6 +11,7 @@ All notable changes to Gogs are documented in this file.
 ### Removed
 ### Removed
 
 
 - The `gogs cert` subcommand. [#8153](https://github.com/gogs/gogs/pull/8153)
 - 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
 ## 0.14.1
 
 

+ 0 - 2
conf/app.ini

@@ -197,8 +197,6 @@ USER = noreply@gogs.localhost
 ; The login password.
 ; The login password.
 PASSWORD =
 PASSWORD =
 
 
-; Whether to disable HELO operation when the hostname is different.
-DISABLE_HELO =
 ; The custom hostname for HELO operation, default is from system.
 ; The custom hostname for HELO operation, default is from system.
 HELO_HOSTNAME =
 HELO_HOSTNAME =
 
 

+ 0 - 1
conf/locale/locale_en-US.ini

@@ -1262,7 +1262,6 @@ config.email.subject_prefix = Subject prefix
 config.email.host = Host
 config.email.host = Host
 config.email.from = From
 config.email.from = From
 config.email.user = User
 config.email.user = User
-config.email.disable_helo = Disable HELO
 config.email.helo_hostname = HELO hostname
 config.email.helo_hostname = HELO hostname
 config.email.skip_verify = Skip certificate verify
 config.email.skip_verify = Skip certificate verify
 config.email.use_certificate = Use custom certificate
 config.email.use_certificate = Use custom certificate

+ 1 - 2
go.mod

@@ -43,11 +43,11 @@ require (
 	github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6
 	github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6
 	github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e
 	github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e
 	github.com/urfave/cli/v3 v3.6.2
 	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/crypto v0.47.0
 	golang.org/x/image v0.35.0
 	golang.org/x/image v0.35.0
 	golang.org/x/net v0.48.0
 	golang.org/x/net v0.48.0
 	golang.org/x/text v0.33.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/ini.v1 v1.67.0
 	gopkg.in/macaron.v1 v1.5.1
 	gopkg.in/macaron.v1 v1.5.1
 	gorm.io/driver/mysql v1.5.2
 	gorm.io/driver/mysql v1.5.2
@@ -132,7 +132,6 @@ require (
 	golang.org/x/sync v0.19.0 // indirect
 	golang.org/x/sync v0.19.0 // indirect
 	golang.org/x/sys v0.40.0 // indirect
 	golang.org/x/sys v0.40.0 // indirect
 	google.golang.org/protobuf v1.36.6 // 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/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect
 	gopkg.in/redis.v2 v2.3.2 // indirect
 	gopkg.in/redis.v2 v2.3.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 2 - 4
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/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 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
 github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
 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 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 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/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 h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU=
 gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4=
 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=
 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 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/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.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.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=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=

+ 0 - 1
internal/conf/static.go

@@ -56,7 +56,6 @@ var (
 		User          string
 		User          string
 		Password      string
 		Password      string
 
 
-		DisableHELO  bool   `ini:"DISABLE_HELO"`
 		HELOHostname string `ini:"HELO_HOSTNAME"`
 		HELOHostname string `ini:"HELO_HOSTNAME"`
 
 
 		SkipVerify     bool
 		SkipVerify     bool

+ 0 - 1
internal/conf/testdata/TestInit.golden.ini

@@ -88,7 +88,6 @@ HOST=smtp.mailgun.org:587
 FROM=noreply@gogs.localhost
 FROM=noreply@gogs.localhost
 USER=noreply@gogs.localhost
 USER=noreply@gogs.localhost
 PASSWORD=87654321
 PASSWORD=87654321
-DISABLE_HELO=false
 HELO_HOSTNAME=
 HELO_HOSTNAME=
 SKIP_VERIFY=false
 SKIP_VERIFY=false
 USE_CERTIFICATE=false
 USE_CERTIFICATE=false

+ 6 - 2
internal/database/issue_mail.go

@@ -151,7 +151,9 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
 			names = append(names, issue.Assignee.Name)
 			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.
 	// Mail mentioned people and exclude watchers.
 	names = append(names, doer.Name)
 	names = append(names, doer.Name)
@@ -168,7 +170,9 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
 	if err != nil {
 	if err != nil {
 		return errors.Wrap(err, "get mailable emails by usernames")
 		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
 	return nil
 }
 }
 
 

+ 72 - 46
internal/email/email.go

@@ -3,13 +3,13 @@ package email
 import (
 import (
 	"fmt"
 	"fmt"
 	"html/template"
 	"html/template"
+	"net/mail"
 	"path/filepath"
 	"path/filepath"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-	"gopkg.in/gomail.v2"
+	"github.com/cockroachdb/errors"
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
-	log "unknwon.dev/clog/v2"
 
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/markup"
 	"gogs.io/gogs/internal/markup"
@@ -72,7 +72,11 @@ func render(tpl string, data map[string]any) (string, error) {
 }
 }
 
 
 func SendTestMail(email 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
 	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{
 	data := map[string]any{
 		"Username":          u.DisplayName(),
 		"Username":          u.DisplayName(),
 		"ActiveCodeLives":   conf.Auth.ActivateCodeLives / 60,
 		"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)
 	body, err := render(tpl, data)
 	if err != nil {
 	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{
 	data := map[string]any{
 		"Username":        u.DisplayName(),
 		"Username":        u.DisplayName(),
 		"ActiveCodeLives": conf.Auth.ActivateCodeLives / 60,
 		"ActiveCodeLives": conf.Auth.ActivateCodeLives / 60,
@@ -135,35 +141,39 @@ func SendActivateEmailMail(c *macaron.Context, u User, email string) {
 	}
 	}
 	body, err := render(tmplAuthActivateEmail, data)
 	body, err := render(tmplAuthActivateEmail, data)
 	if err != nil {
 	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{
 	data := map[string]any{
 		"Username": u.DisplayName(),
 		"Username": u.DisplayName(),
 	}
 	}
 	body, err := render(tmplAuthRegisterNotify, data)
 	body, err := render(tmplAuthRegisterNotify, data)
 	if err != nil {
 	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())
 	subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repo.FullName())
 
 
 	data := map[string]any{
 	data := map[string]any{
@@ -173,14 +183,17 @@ func SendCollaboratorMail(u, doer User, repo Repository) {
 	}
 	}
 	body, err := render(tmplNotifyCollaborator, data)
 	body, err := render(tmplNotifyCollaborator, data)
 	if err != nil {
 	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 {
 func composeTplData(subject, body, link string) map[string]any {
@@ -191,34 +204,47 @@ func composeTplData(subject, body, link string) map[string]any {
 	return data
 	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()
 	subject := issue.MailSubject()
 	body := string(markup.Markdown([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas()))
 	body := string(markup.Markdown([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas()))
 	data := composeTplData(subject, body, issue.HTMLURL())
 	data := composeTplData(subject, body, issue.HTMLURL())
 	data["Doer"] = doer
 	data["Doer"] = doer
 	content, err := render(tplName, data)
 	content, err := render(tplName, data)
 	if err != nil {
 	if err != nil {
-		log.Error("HTMLString (%s): %v", tplName, err)
+		return nil, errors.Wrapf(err, "render %q", tplName)
+	}
+	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")
 	}
 	}
-	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
+	msg.info = fmt.Sprintf("Subject: %s, %s", subject, info)
+	return msg, nil
 }
 }
 
 
 // SendIssueCommentMail composes and sends issue comment emails to target receivers.
 // 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 {
 	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.
 // 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 {
 	if len(tos) == 0 {
-		return
+		return nil
+	}
+	msg, err := composeIssueMessage(issue, repo, doer, tmplIssueMention, tos, "issue mention")
+	if err != nil {
+		return errors.Wrap(err, "compose issue message")
 	}
 	}
-	Send(composeIssueMessage(issue, repo, doer, tmplIssueMention, tos, "issue mention"))
+	send(msg)
+	return nil
 }
 }

+ 71 - 151
internal/email/message.go

@@ -2,214 +2,134 @@ package email
 
 
 import (
 import (
 	"crypto/tls"
 	"crypto/tls"
-	"io"
 	"net"
 	"net"
-	"net/smtp"
-	"os"
+	"strconv"
 	"strings"
 	"strings"
-	"time"
 
 
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/errors"
 	"github.com/inbucket/html2text"
 	"github.com/inbucket/html2text"
-	"gopkg.in/gomail.v2"
+	gomail "github.com/wneessen/go-mail"
 	log "unknwon.dev/clog/v2"
 	log "unknwon.dev/clog/v2"
 
 
 	"gogs.io/gogs/internal/conf"
 	"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{}
 	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)
 	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 {
 	if conf.Email.UsePlainText || conf.Email.AddPlainTextAlt {
 		plainBody, err := html2text.FromString(htmlBody)
 		plainBody, err := html2text.FromString(htmlBody)
 		if err != nil {
 		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 {
 		} 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,
-		confirmChan: make(chan struct{}),
-	}
-}
 
 
-// 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)
-}
-
-type loginAuth struct {
-	username, password string
+	return &message{
+		msg:         m,
+		confirmChan: make(chan struct{}),
+	}, nil
 }
 }
 
 
-// SMTP AUTH LOGIN Auth Handler
-func LoginAuth(username, password string) smtp.Auth {
-	return &loginAuth{username, password}
+func newMessage(to []string, subject, body string) (*message, error) {
+	return newMessageFrom(to, conf.Email.From, subject, body)
 }
 }
 
 
-func (*loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
-	return "LOGIN", []byte{}, nil
-}
+func newSMTPClient() (*gomail.Client, error) {
+	opts := conf.Email
 
 
-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))
-		}
+	host, portStr, err := net.SplitHostPort(opts.Host)
+	if err != nil {
+		return nil, err
+	}
+	port, err := strconv.Atoi(portStr)
+	if err != nil {
+		return nil, err
 	}
 	}
-	return nil, nil
-}
 
 
-type Sender struct{}
+	clientOpts := []gomail.Option{
+		gomail.WithPort(port),
+	}
 
 
-func (*Sender) Send(from string, to []string, msg io.WriterTo) error {
-	opts := conf.Email
+	if port == 465 {
+		clientOpts = append(clientOpts, gomail.WithSSL())
+	} else {
+		clientOpts = append(clientOpts, gomail.WithTLSPolicy(gomail.TLSOpportunistic))
+	}
 
 
-	host, port, err := net.SplitHostPort(opts.Host)
-	if err != nil {
-		return err
+	if opts.HELOHostname != "" {
+		clientOpts = append(clientOpts, gomail.WithHELO(opts.HELOHostname))
 	}
 	}
 
 
 	tlsconfig := &tls.Config{
 	tlsconfig := &tls.Config{
 		InsecureSkipVerify: opts.SkipVerify,
 		InsecureSkipVerify: opts.SkipVerify,
 		ServerName:         host,
 		ServerName:         host,
 	}
 	}
-
 	if opts.UseCertificate {
 	if opts.UseCertificate {
 		cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
 		cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
 		if err != nil {
 		if err != nil {
-			return err
+			return nil, err
 		}
 		}
 		tlsconfig.Certificates = []tls.Certificate{cert}
 		tlsconfig.Certificates = []tls.Certificate{cert}
 	}
 	}
+	clientOpts = append(clientOpts, gomail.WithTLSConfig(tlsconfig))
 
 
-	conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
-	if err != nil {
-		return err
+	if len(opts.User) > 0 {
+		clientOpts = append(clientOpts,
+			gomail.WithSMTPAuth(gomail.SMTPAuthAutoDiscover),
+			gomail.WithUsername(opts.User),
+			gomail.WithPassword(opts.Password),
+		)
 	}
 	}
-	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)
-		}
-	}
+	return gomail.NewClient(host, clientOpts...)
+}
 
 
-	w, err := client.Data()
+func sendMessage(msg *message) error {
+	client, err := newSMTPClient()
 	if err != nil {
 	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 err
 	}
 	}
-
-	return client.Quit()
+	return client.DialAndSend(msg.msg)
 }
 }
 
 
 func processMailQueue() {
 func processMailQueue() {
-	sender := &Sender{}
 	for msg := range mailQueue {
 	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 {
 		} 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{}{}
 		msg.confirmChan <- struct{}{}
 	}
 	}
 }
 }
 
 
-var mailQueue chan *Message
+var mailQueue chan *message
 
 
 // NewContext initializes settings for mailer.
 // NewContext initializes settings for mailer.
 func NewContext() {
 func NewContext() {
@@ -220,14 +140,14 @@ func NewContext() {
 		return
 		return
 	}
 	}
 
 
-	mailQueue = make(chan *Message, 1000)
+	mailQueue = make(chan *message, 1000)
 	go processMailQueue()
 	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,
 // It returns without confirmation (mail processed asynchronously) in normal cases,
 // but waits/blocks under hook mode to make sure mail has been sent.
 // 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 {
 	if !conf.Email.Enabled {
 		return
 		return
 	}
 	}

+ 3 - 1
internal/route/admin/users.go

@@ -106,7 +106,9 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 
 
 	// Send email notification.
 	// Send email notification.
 	if f.SendNotify && conf.Email.Enabled {
 	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))
 	c.Flash.Success(c.Tr("admin.users.new_success", user.Name))

+ 3 - 1
internal/route/api/v1/admin_user.go

@@ -69,7 +69,9 @@ func adminCreateUser(c *context.APIContext, form adminCreateUserRequest) {
 
 
 	// Send email notification.
 	// Send email notification.
 	if form.SendNotify && conf.Email.Enabled {
 	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))
 	c.JSON(http.StatusCreated, toUser(u))

+ 3 - 1
internal/route/repo/setting.go

@@ -401,7 +401,9 @@ func SettingsCollaborationPost(c *context.Context) {
 	}
 	}
 
 
 	if conf.User.EnableEmailNotification {
 	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"))
 	c.Flash.Success(c.Tr("repo.settings.add_collaborator_success"))

+ 9 - 3
internal/route/user/auth.go

@@ -385,7 +385,9 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
 
 
 	// Send confirmation email.
 	// Send confirmation email.
 	if conf.Auth.RequireEmailConfirmation && user.ID > 1 {
 	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["IsSendRegisterMail"] = true
 		c.Data["Email"] = user.Email
 		c.Data["Email"] = user.Email
 		c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
 		c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
@@ -469,7 +471,9 @@ func Activate(c *context.Context) {
 				c.Data["ResendLimited"] = true
 				c.Data["ResendLimited"] = true
 			} else {
 			} else {
 				c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
 				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 {
 				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)
 					log.Error("Failed to put cache key 'mail resend': %v", err)
@@ -579,7 +583,9 @@ func ForgotPasswdPost(c *context.Context) {
 		return
 		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 {
 	if err = c.Cache.Put(userutil.MailResendCacheKey(u.ID), 1, 180); err != nil {
 		log.Error("Failed to put cache key 'mail resend': %v", err)
 		log.Error("Failed to put cache key 'mail resend': %v", err)
 	}
 	}

+ 3 - 1
internal/route/user/setting.go

@@ -280,7 +280,9 @@ func SettingsEmailPost(c *context.Context, f form.AddEmail) {
 
 
 	// Send confirmation email
 	// Send confirmation email
 	if conf.Auth.RequireEmailConfirmation {
 	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 {
 		if err := c.Cache.Put("MailResendLimit_"+c.User.LowerName, c.User.LowerName, 180); err != nil {
 			log.Error("Set cache 'MailResendLimit' failed: %v", err)
 			log.Error("Set cache 'MailResendLimit' failed: %v", err)

+ 0 - 2
templates/admin/config.tmpl

@@ -237,8 +237,6 @@
 
 
 							<div class="ui divider"></div>
 							<div class="ui divider"></div>
 
 
-							<dt>{{.i18n.Tr "admin.config.email.disable_helo"}}</dt>
-							<dd><i class="fa fa{{if .Email.DisableHELO}}-check{{end}}-square-o"></i></dd>
 							<dt>{{.i18n.Tr "admin.config.email.helo_hostname"}}</dt>
 							<dt>{{.i18n.Tr "admin.config.email.helo_hostname"}}</dt>
 							<dd>
 							<dd>
 								{{if .Email.HELOHostname}}
 								{{if .Email.HELOHostname}}