mirror of
https://github.com/gogs/gogs.git
synced 2026-06-17 21:50:28 +02:00
email: replace gomail with go-mail (#8164)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
3
go.mod
3
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
@@ -56,7 +56,6 @@ var (
|
||||
User string
|
||||
Password string
|
||||
|
||||
DisableHELO bool `ini:"DISABLE_HELO"`
|
||||
HELOHostname string `ini:"HELO_HOSTNAME"`
|
||||
|
||||
SkipVerify bool
|
||||
|
||||
1
internal/conf/testdata/TestInit.golden.ini
vendored
1
internal/conf/testdata/TestInit.golden.ini
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -237,8 +237,6 @@
|
||||
|
||||
<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>
|
||||
<dd>
|
||||
{{if .Email.HELOHostname}}
|
||||
|
||||
Reference in New Issue
Block a user