Browse Source

switch from go-simple-mail to go-mail

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 2 years ago
parent
commit
f2618e7de6

+ 9 - 12
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

File diff suppressed because it is too large
+ 427 - 85
go.sum


+ 4 - 4
internal/common/dataretention.go

@@ -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
 }
 

+ 2 - 0
internal/common/dataretention_test.go

@@ -84,6 +84,7 @@ func TestRetentionValidation(t *testing.T) {
 	smtpCfg := smtp.Config{
 		Host:          "mail.example.com",
 		Port:          25,
+		From:          "[email protected]",
 		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:          "[email protected]",
 		TemplatesPath: "templates",
 	}
 	err := smtpCfg.Initialize(configDir)

+ 64 - 52
internal/common/eventmanager.go

@@ -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)
+	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 {
-		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))
-		}
 		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) {
-	reader, cancelFn, err := getFileReader(conn, virtualPath)
-	if err != nil {
-		return nil, err
-	}
+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 0, err
+		}
 
-	defer cancelFn()
-	defer reader.Close()
+		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{
-			Name: path.Base(virtualPath),
-			Data: data,
+		files = append(files, &mail.File{
+			Name:   path.Base(virtualPath),
+			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
 		}

+ 26 - 18
internal/common/eventmanager_test.go

@@ -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)
-	assert.Error(t, err)
+	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/",

+ 7 - 0
internal/httpd/httpd_test.go

@@ -5973,6 +5973,7 @@ func TestNamingRules(t *testing.T) {
 	smtpCfg := smtp.Config{
 		Host:          "127.0.0.1",
 		Port:          3525,
+		From:          "[email protected]",
 		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:          "[email protected]",
 		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:          "[email protected]",
 		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:          "[email protected]",
 		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:          "[email protected]",
 		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:          "[email protected]",
 		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:          "[email protected]",
 		TemplatesPath: "templates",
 	}
 	err := smtpCfg.Initialize(configDir)

+ 65 - 62
internal/smtp/smtp.go

@@ -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,53 +105,51 @@ 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))
 	}
-}
-
-func (c *Config) getAuthType() mail.AuthType {
-	if c.User == "" && c.Password == "" {
-		return mail.AuthNone
+	if config.User != "" {
+		options = append(options, mail.WithUsername(config.User))
 	}
-	switch c.AuthType {
-	case 1:
-		return mail.AuthLogin
-	case 2:
-		return mail.AuthCRAMMD5
-	default:
-		return mail.AuthPlain
+	if config.Password != "" {
+		options = append(options, mail.WithPassword(config.Password))
+	}
+	if config.User != "" || config.Password != "" {
+		switch config.AuthType {
+		case 1:
+			options = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin))
+		case 2:
+			options = append(options, mail.WithSMTPAuth(mail.SMTPAuthCramMD5))
+		default:
+			options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain))
+		}
 	}
+	if config.Domain != "" {
+		options = append(options, mail.WithHELO(config.Domain))
+	}
+	return options
 }
 
 func loadTemplates(templatesPath string) {
@@ -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
+	}
+	if err := m.From(from); err != nil {
+		return fmt.Errorf("invalid from address: %w", err)
 	}
-	email.AddTo(to...).SetSubject(subject)
+	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)
-	}
-	if email.Error != nil {
-		return fmt.Errorf("smtp: email error: %w", email.Error)
+
+	c, err := mail.NewClient(config.Host, config.getMailClientOptions()...)
+	if err != nil {
+		return fmt.Errorf("unable to create mail client: %w", err)
 	}
-	return email.Send(smtpClient)
+	ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancelFn()
+
+	return c.DialAndSendWithContext(ctx, m)
 }

Some files were not shown because too many files changed in this diff