switch from go-simple-mail to go-mail
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
6afbd77fd5
commit
f2618e7de6
8 changed files with 716 additions and 278 deletions
21
go.mod
21
go.mod
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.47
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.20
|
||||
|
@ -59,27 +59,27 @@ require (
|
|||
github.com/spf13/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012
|
||||
github.com/subosito/gotenv v1.4.1
|
||||
github.com/subosito/gotenv v1.4.2
|
||||
github.com/unrolled/secure v1.13.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0
|
||||
github.com/wneessen/go-mail v0.3.8
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/automaxprocs v1.5.1
|
||||
gocloud.dev v0.27.0
|
||||
gocloud.dev v0.28.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/net v0.5.0
|
||||
golang.org/x/oauth2 v0.4.0
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/term v0.4.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/api v0.106.0
|
||||
google.golang.org/api v0.107.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.108.0 // indirect
|
||||
cloud.google.com/go/compute v1.15.0 // indirect
|
||||
cloud.google.com/go/compute v1.15.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
|
||||
|
@ -108,7 +108,6 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-test/deep v1.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
|
@ -129,8 +128,7 @@ require (
|
|||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.7 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
|
@ -153,7 +151,6 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
|
@ -161,8 +158,8 @@ require (
|
|||
golang.org/x/tools v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230106154932-a12b697841d9 // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 // indirect
|
||||
google.golang.org/grpc v1.52.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
|
|
@ -29,7 +29,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/command"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
@ -372,7 +372,7 @@ func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
|
|||
Results: c.results,
|
||||
})
|
||||
}
|
||||
var files []mail.File
|
||||
var files []*mail.File
|
||||
f, err := params.getRetentionReportsAsMailAttachment()
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "unable to get retention report as mail attachment: %v", err)
|
||||
|
@ -391,11 +391,11 @@ func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
|
|||
body := "Further details attached."
|
||||
err = smtp.SendEmail([]string{c.Email}, subject, body, smtp.EmailContentTypeTextPlain, files...)
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %v", err,
|
||||
c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %s", err,
|
||||
time.Since(startTime))
|
||||
return err
|
||||
}
|
||||
c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %v", time.Since(startTime))
|
||||
c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %s", time.Since(startTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ func TestRetentionValidation(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "mail.example.com",
|
||||
Port: 25,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
|
@ -116,6 +117,7 @@ func TestRetentionEmailNotifications(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
|
|
|
@ -41,7 +41,7 @@ import (
|
|||
"github.com/robfig/cron/v3"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
|
@ -563,16 +563,30 @@ func (p *EventParams) getCompressedDataRetentionReport() ([]byte, error) {
|
|||
return nil, errors.New("no data retention report available")
|
||||
}
|
||||
var b bytes.Buffer
|
||||
wr := zip.NewWriter(&b)
|
||||
for _, check := range p.retentionChecks {
|
||||
if size := int64(len(b.Bytes())); size > maxAttachmentsSize {
|
||||
eventManagerLog(logger.LevelError, "unable to get retention report, size too large: %s", util.ByteCountIEC(size))
|
||||
return nil, fmt.Errorf("unable to get retention report, size too large: %s", util.ByteCountIEC(size))
|
||||
if _, err := p.writeCompressedDataRetentionReports(&b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (p *EventParams) writeCompressedDataRetentionReports(w io.Writer) (int64, error) {
|
||||
var n int64
|
||||
wr := zip.NewWriter(w)
|
||||
|
||||
for _, check := range p.retentionChecks {
|
||||
data, err := getCSVRetentionReport(check.Results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get CSV report: %w", err)
|
||||
return n, fmt.Errorf("unable to get CSV report: %w", err)
|
||||
}
|
||||
dataSize := int64(len(data))
|
||||
n += dataSize
|
||||
// we suppose a 3:1 compression ratio
|
||||
if n > (maxAttachmentsSize * 3) {
|
||||
eventManagerLog(logger.LevelError, "unable to get retention report, size too large: %s",
|
||||
util.ByteCountIEC(n))
|
||||
return n, fmt.Errorf("unable to get retention report, size too large: %s", util.ByteCountIEC(n))
|
||||
}
|
||||
|
||||
fh := &zip.FileHeader{
|
||||
Name: fmt.Sprintf("%s-%s.csv", check.ActionName, check.Username),
|
||||
Method: zip.Deflate,
|
||||
|
@ -580,28 +594,28 @@ func (p *EventParams) getCompressedDataRetentionReport() ([]byte, error) {
|
|||
}
|
||||
f, err := wr.CreateHeader(fh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create zip header for file %q: %w", fh.Name, err)
|
||||
return n, fmt.Errorf("unable to create zip header for file %q: %w", fh.Name, err)
|
||||
}
|
||||
_, err = io.Copy(f, bytes.NewBuffer(data))
|
||||
_, err = io.CopyN(f, bytes.NewBuffer(data), dataSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to write content to zip file %q: %w", fh.Name, err)
|
||||
return n, fmt.Errorf("unable to write content to zip file %q: %w", fh.Name, err)
|
||||
}
|
||||
}
|
||||
if err := wr.Close(); err != nil {
|
||||
return nil, fmt.Errorf("unable to close zip writer: %w", err)
|
||||
return n, fmt.Errorf("unable to close zip writer: %w", err)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (p *EventParams) getRetentionReportsAsMailAttachment() (mail.File, error) {
|
||||
var result mail.File
|
||||
data, err := p.getCompressedDataRetentionReport()
|
||||
if err != nil {
|
||||
return result, err
|
||||
func (p *EventParams) getRetentionReportsAsMailAttachment() (*mail.File, error) {
|
||||
if len(p.retentionChecks) == 0 {
|
||||
return nil, errors.New("no data retention report available")
|
||||
}
|
||||
result.Name = "retention-reports.zip"
|
||||
result.Data = data
|
||||
return result, nil
|
||||
return &mail.File{
|
||||
Name: "retention-reports.zip",
|
||||
Header: make(map[string][]string),
|
||||
Writer: p.writeCompressedDataRetentionReports,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *EventParams) getStringReplacements(addObjectData bool) []string {
|
||||
|
@ -905,34 +919,24 @@ func writeFileContent(conn *BaseConnection, virtualPath string, w io.Writer) err
|
|||
return err
|
||||
}
|
||||
|
||||
func getFileContent(conn *BaseConnection, virtualPath string, expectedSize int) ([]byte, error) {
|
||||
func getFileContentFn(conn *BaseConnection, virtualPath string, size int64) func(w io.Writer) (int64, error) {
|
||||
return func(w io.Writer) (int64, error) {
|
||||
reader, cancelFn, err := getFileReader(conn, virtualPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer cancelFn()
|
||||
defer reader.Close()
|
||||
|
||||
data := make([]byte, expectedSize)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
return data, err
|
||||
return io.CopyN(w, reader, size)
|
||||
}
|
||||
}
|
||||
|
||||
func getMailAttachments(user dataprovider.User, attachments []string, replacer *strings.Replacer) ([]mail.File, error) {
|
||||
var files []mail.File
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
func getMailAttachments(conn *BaseConnection, attachments []string, replacer *strings.Replacer) ([]*mail.File, error) {
|
||||
var files []*mail.File
|
||||
totalSize := int64(0)
|
||||
|
||||
for _, virtualPath := range replacePathsPlaceholders(attachments, replacer) {
|
||||
info, err := conn.DoStat(virtualPath, 0, false)
|
||||
if err != nil {
|
||||
|
@ -945,13 +949,10 @@ func getMailAttachments(user dataprovider.User, attachments []string, replacer *
|
|||
if totalSize > maxAttachmentsSize {
|
||||
return nil, fmt.Errorf("unable to send files as attachment, size too large: %s", util.ByteCountIEC(totalSize))
|
||||
}
|
||||
data, err := getFileContent(conn, virtualPath, int(info.Size()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get content for file %q, user %q: %w", virtualPath, conn.User.Username, err)
|
||||
}
|
||||
files = append(files, mail.File{
|
||||
files = append(files, &mail.File{
|
||||
Name: path.Base(virtualPath),
|
||||
Data: data,
|
||||
Header: make(map[string][]string),
|
||||
Writer: getFileContentFn(conn, virtualPath, info.Size()),
|
||||
})
|
||||
}
|
||||
return files, nil
|
||||
|
@ -1265,7 +1266,7 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
body := replaceWithReplacer(c.Body, replacer)
|
||||
subject := replaceWithReplacer(c.Subject, replacer)
|
||||
startTime := time.Now()
|
||||
var files []mail.File
|
||||
var files []*mail.File
|
||||
fileAttachments := make([]string, 0, len(c.Attachments))
|
||||
for _, attachment := range c.Attachments {
|
||||
if attachment == dataprovider.RetentionReportPlaceHolder {
|
||||
|
@ -1283,7 +1284,18 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := getMailAttachments(user, fileAttachments, replacer)
|
||||
user, err = getUserForEventAction(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
res, err := getMailAttachments(conn, fileAttachments, replacer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
sdkkms "github.com/sftpgo/sdk/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -530,14 +531,6 @@ func TestEventManagerErrors(t *testing.T) {
|
|||
},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
_, err = getMailAttachments(dataprovider.User{
|
||||
Groups: []sdk.GroupMapping{
|
||||
{
|
||||
Name: groupName,
|
||||
Type: sdk.GroupTypePrimary,
|
||||
},
|
||||
}}, []string{"/a", "/b"}, nil)
|
||||
assert.Error(t, err)
|
||||
err = executePwdExpirationCheckForUser(&dataprovider.User{
|
||||
Groups: []sdk.GroupMapping{
|
||||
{
|
||||
|
@ -1253,17 +1246,21 @@ func TestGetFileContent(t *testing.T) {
|
|||
fileContent := []byte("test file content")
|
||||
err = os.WriteFile(filepath.Join(user.GetHomeDir(), "file.txt"), fileContent, 0666)
|
||||
assert.NoError(t, err)
|
||||
conn := NewBaseConnection(xid.New().String(), protocolEventAction, "", "", user)
|
||||
replacer := strings.NewReplacer("old", "new")
|
||||
files, err := getMailAttachments(user, []string{"/file.txt"}, replacer)
|
||||
files, err := getMailAttachments(conn, []string{"/file.txt"}, replacer)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, files, 1) {
|
||||
assert.Equal(t, fileContent, files[0].Data)
|
||||
var b bytes.Buffer
|
||||
_, err = files[0].Writer(&b)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fileContent, b.Bytes())
|
||||
}
|
||||
// missing file
|
||||
_, err = getMailAttachments(user, []string{"/file1.txt"}, replacer)
|
||||
_, err = getMailAttachments(conn, []string{"/file1.txt"}, replacer)
|
||||
assert.Error(t, err)
|
||||
// directory
|
||||
_, err = getMailAttachments(user, []string{"/"}, replacer)
|
||||
_, err = getMailAttachments(conn, []string{"/"}, replacer)
|
||||
assert.Error(t, err)
|
||||
// files too large
|
||||
content := make([]byte, maxAttachmentsSize/2+1)
|
||||
|
@ -1273,12 +1270,15 @@ func TestGetFileContent(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(user.GetHomeDir(), "file2.txt"), content, 0666)
|
||||
assert.NoError(t, err)
|
||||
files, err = getMailAttachments(user, []string{"/file1.txt"}, replacer)
|
||||
files, err = getMailAttachments(conn, []string{"/file1.txt"}, replacer)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, files, 1) {
|
||||
assert.Equal(t, content, files[0].Data)
|
||||
var b bytes.Buffer
|
||||
_, err = files[0].Writer(&b)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, content, b.Bytes())
|
||||
}
|
||||
_, err = getMailAttachments(user, []string{"/file1.txt", "/file2.txt"}, replacer)
|
||||
_, err = getMailAttachments(conn, []string{"/file1.txt", "/file2.txt"}, replacer)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "size too large")
|
||||
}
|
||||
|
@ -1287,9 +1287,15 @@ func TestGetFileContent(t *testing.T) {
|
|||
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("pwd")
|
||||
err = dataprovider.UpdateUser(&user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
conn = NewBaseConnection(xid.New().String(), protocolEventAction, "", "", user)
|
||||
// the file is not encrypted so reading the encryption header will fail
|
||||
_, err = getMailAttachments(user, []string{"/file.txt"}, replacer)
|
||||
files, err = getMailAttachments(conn, []string{"/file.txt"}, replacer)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, files, 1) {
|
||||
var b bytes.Buffer
|
||||
_, err = files[0].Writer(&b)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
err = dataprovider.DeleteUser(username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
@ -1361,7 +1367,9 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
sender: username,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
_, err = getFileContent(NewBaseConnection("", protocolEventAction, "", "", user), "/f.txt", 1234)
|
||||
fn := getFileContentFn(NewBaseConnection("", protocolEventAction, "", "", user), "/f.txt", 1234)
|
||||
var b bytes.Buffer
|
||||
_, err = fn(&b)
|
||||
assert.Error(t, err)
|
||||
err = executeHTTPRuleAction(dataprovider.EventActionHTTPConfig{
|
||||
Endpoint: "http://127.0.0.1:9999/",
|
||||
|
|
|
@ -5973,6 +5973,7 @@ func TestNamingRules(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
|
@ -11655,6 +11656,7 @@ func TestMaxSessions(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
|
@ -11732,6 +11734,7 @@ func TestSFTPLoopError(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
|
@ -21672,6 +21675,7 @@ func TestAdminForgotPassword(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
|
@ -21777,6 +21781,7 @@ func TestAdminForgotPassword(t *testing.T) {
|
|||
smtpCfg = smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3526,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
|
@ -21825,6 +21830,7 @@ func TestUserForgotPassword(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
|
@ -21975,6 +21981,7 @@ func TestAPIForgotPassword(t *testing.T) {
|
|||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
|
|
|
@ -17,13 +17,14 @@ package smtp
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
@ -49,14 +50,13 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
smtpServer *mail.SMTPServer
|
||||
from string
|
||||
config *Config
|
||||
emailTemplates = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
// IsEnabled returns true if an SMTP server is configured
|
||||
func IsEnabled() bool {
|
||||
return smtpServer != nil
|
||||
return config != nil
|
||||
}
|
||||
|
||||
// Config defines the SMTP configuration to use to send emails
|
||||
|
@ -91,7 +91,7 @@ type Config struct {
|
|||
|
||||
// Initialize initialized and validates the SMTP configuration
|
||||
func (c *Config) Initialize(configDir string) error {
|
||||
smtpServer = nil
|
||||
config = nil
|
||||
if c.Host == "" {
|
||||
logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
|
||||
return nil
|
||||
|
@ -105,54 +105,52 @@ func (c *Config) Initialize(configDir string) error {
|
|||
if c.Encryption < 0 || c.Encryption > 2 {
|
||||
return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
|
||||
}
|
||||
if c.From == "" && c.User == "" {
|
||||
return fmt.Errorf(`smtp: from address and user cannot both be empty`)
|
||||
}
|
||||
templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
|
||||
if templatesPath == "" {
|
||||
return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
|
||||
}
|
||||
loadTemplates(filepath.Join(templatesPath, templateEmailDir))
|
||||
from = c.From
|
||||
smtpServer = mail.NewSMTPClient()
|
||||
smtpServer.Host = c.Host
|
||||
smtpServer.Port = c.Port
|
||||
smtpServer.Username = c.User
|
||||
smtpServer.Password = c.Password
|
||||
smtpServer.Authentication = c.getAuthType()
|
||||
smtpServer.Encryption = c.getEncryption()
|
||||
smtpServer.KeepAlive = false
|
||||
smtpServer.ConnectTimeout = 10 * time.Second
|
||||
smtpServer.SendTimeout = 120 * time.Second
|
||||
if c.Domain != "" {
|
||||
smtpServer.Helo = c.Domain
|
||||
}
|
||||
logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
|
||||
smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
|
||||
config = c
|
||||
logger.Debug(logSender, "", "configuration successfully initialized, host: %q, port: %d, username: %q, auth: %d, encryption: %d, helo: %q",
|
||||
config.Host, config.Port, config.User, config.AuthType, config.Encryption, config.Domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) getEncryption() mail.Encryption {
|
||||
func (c *Config) getMailClientOptions() []mail.Option {
|
||||
options := []mail.Option{mail.WithPort(c.Port)}
|
||||
|
||||
switch c.Encryption {
|
||||
case 1:
|
||||
return mail.EncryptionSSLTLS
|
||||
options = append(options, mail.WithSSL())
|
||||
case 2:
|
||||
return mail.EncryptionSTARTTLS
|
||||
options = append(options, mail.WithTLSPolicy(mail.TLSMandatory))
|
||||
default:
|
||||
return mail.EncryptionNone
|
||||
options = append(options, mail.WithTLSPolicy(mail.NoTLS))
|
||||
}
|
||||
if config.User != "" {
|
||||
options = append(options, mail.WithUsername(config.User))
|
||||
}
|
||||
|
||||
func (c *Config) getAuthType() mail.AuthType {
|
||||
if c.User == "" && c.Password == "" {
|
||||
return mail.AuthNone
|
||||
if config.Password != "" {
|
||||
options = append(options, mail.WithPassword(config.Password))
|
||||
}
|
||||
switch c.AuthType {
|
||||
if config.User != "" || config.Password != "" {
|
||||
switch config.AuthType {
|
||||
case 1:
|
||||
return mail.AuthLogin
|
||||
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin))
|
||||
case 2:
|
||||
return mail.AuthCRAMMD5
|
||||
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthCramMD5))
|
||||
default:
|
||||
return mail.AuthPlain
|
||||
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain))
|
||||
}
|
||||
}
|
||||
if config.Domain != "" {
|
||||
options = append(options, mail.WithHELO(config.Domain))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func loadTemplates(templatesPath string) {
|
||||
logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
|
||||
|
@ -168,7 +166,7 @@ func loadTemplates(templatesPath string) {
|
|||
|
||||
// RenderPasswordResetTemplate executes the password reset template
|
||||
func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
|
||||
if smtpServer == nil {
|
||||
if !IsEnabled() {
|
||||
return errors.New("smtp: not configured")
|
||||
}
|
||||
return emailTemplates[templatePasswordReset].Execute(buf, data)
|
||||
|
@ -176,46 +174,51 @@ func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
|
|||
|
||||
// RenderPasswordExpirationTemplate executes the password expiration template
|
||||
func RenderPasswordExpirationTemplate(buf *bytes.Buffer, data any) error {
|
||||
if smtpServer == nil {
|
||||
if !IsEnabled() {
|
||||
return errors.New("smtp: not configured")
|
||||
}
|
||||
return emailTemplates[templatePasswordExpiration].Execute(buf, data)
|
||||
}
|
||||
|
||||
// SendEmail tries to send an email using the specified parameters.
|
||||
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error {
|
||||
if smtpServer == nil {
|
||||
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {
|
||||
if !IsEnabled() {
|
||||
return errors.New("smtp: not configured")
|
||||
}
|
||||
if len(to) == 0 {
|
||||
return errors.New("smtp: cannot send an email without recipients")
|
||||
}
|
||||
smtpClient, err := smtpServer.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp: unable to connect: %w", err)
|
||||
}
|
||||
m := mail.NewMsg()
|
||||
|
||||
email := mail.NewMSG()
|
||||
email.AllowDuplicateAddress = true
|
||||
if from != "" {
|
||||
email.SetFrom(from)
|
||||
var from string
|
||||
if config.From != "" {
|
||||
from = config.From
|
||||
} else {
|
||||
email.SetFrom(smtpServer.Username)
|
||||
from = config.User
|
||||
}
|
||||
email.AddTo(to...).SetSubject(subject)
|
||||
if err := m.From(from); err != nil {
|
||||
return fmt.Errorf("invalid from address: %w", err)
|
||||
}
|
||||
if err := m.To(to...); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Subject(subject)
|
||||
m.SetDate()
|
||||
m.SetMessageID()
|
||||
m.SetAttachements(attachments)
|
||||
|
||||
switch contentType {
|
||||
case EmailContentTypeTextPlain:
|
||||
email.SetBody(mail.TextPlain, body)
|
||||
m.SetBodyString(mail.TypeTextPlain, body)
|
||||
case EmailContentTypeTextHTML:
|
||||
email.SetBody(mail.TextHTML, body)
|
||||
m.SetBodyString(mail.TypeTextHTML, body)
|
||||
default:
|
||||
return fmt.Errorf("smtp: unsupported body content type %v", contentType)
|
||||
}
|
||||
for _, attachment := range attachments {
|
||||
email.Attach(&attachment)
|
||||
|
||||
c, err := mail.NewClient(config.Host, config.getMailClientOptions()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create mail client: %w", err)
|
||||
}
|
||||
if email.Error != nil {
|
||||
return fmt.Errorf("smtp: email error: %w", email.Error)
|
||||
}
|
||||
return email.Send(smtpClient)
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
return c.DialAndSendWithContext(ctx, m)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue