diff --git a/go.mod b/go.mod index 846fd8c9..9def7be7 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 github.com/unrolled/secure v1.13.0 github.com/wagslane/go-password-validator v0.3.0 - github.com/xhit/go-simple-mail/v2 v2.11.0 + github.com/xhit/go-simple-mail/v2 v2.12.0 github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a go.etcd.io/bbolt v1.3.6 go.uber.org/automaxprocs v1.5.1 @@ -68,16 +68,16 @@ require ( golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 golang.org/x/net v0.0.0-20220923203811-8be639271d50 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 - golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec golang.org/x/time v0.0.0-20220922220347-f3bd1da661af - google.golang.org/api v0.97.0 + google.golang.org/api v0.98.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) require ( cloud.google.com/go v0.104.0 // indirect cloud.google.com/go/compute v1.10.0 // indirect - cloud.google.com/go/iam v0.4.0 // indirect + cloud.google.com/go/iam v0.5.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect @@ -156,7 +156,7 @@ require ( golang.org/x/tools v0.1.12 // 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-20220923205249-dd2d53f1fffc // indirect + google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704 // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 845ba4e0..a4aac07a 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.4.0 h1:YBYU00SCDzZJdHqVc4I5d6lsklcYIjQZa1YmEz4jlSE= -cloud.google.com/go/iam v0.4.0/go.mod h1:cbaZxyScUhxl7ZAkNWiALgihfP75wS/fUsVNaa1r3vA= +cloud.google.com/go/iam v0.5.0 h1:fz9X5zyTWBmamZsqvqZqD7khbifcZF/q+Z1J8pfhIUg= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= @@ -774,8 +774,8 @@ github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFs github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= -github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs= -github.com/xhit/go-simple-mail/v2 v2.11.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= +github.com/xhit/go-simple-mail/v2 v2.12.0 h1:KweA6NO8Z6fZyeckMPNpvElU6QDIyBShlpce1sYUZgg= +github.com/xhit/go-simple-mail/v2 v2.12.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA= github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -981,8 +981,8 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 h1:nwzwVf0l2Y/lkov/+IYgMMbFyI+QypZDds9RxlSmsFQ= -golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1124,8 +1124,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1230,8 +1230,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220923205249-dd2d53f1fffc h1:saaNe2+SBQxandnzcD/qB1JEBQ2Pqew+KlFLLdA/XcM= -google.golang.org/genproto v0.0.0-20220923205249-dd2d53f1fffc/go.mod h1:yEEpwVWKMZZzo81NwRgyEJnA2fQvpXAYPVisv8EgDVs= +google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704 h1:H1AcWFV69NFCMeBJ8nVLtv8uHZZ5Ozcgoq012hHEFuU= +google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/common/dataretention.go b/internal/common/dataretention.go index bde900cc..b58768d2 100644 --- a/internal/common/dataretention.go +++ b/internal/common/dataretention.go @@ -29,6 +29,8 @@ import ( "sync" "time" + mail "github.com/xhit/go-simple-mail/v2" + "github.com/drakkan/sftpgo/v2/internal/command" "github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/httpclient" @@ -350,41 +352,41 @@ func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) { for _, notification := range c.Notifications { switch notification { case RetentionCheckNotificationEmail: - c.sendEmailNotification(elapsed, err) //nolint:errcheck + c.sendEmailNotification(err) //nolint:errcheck case RetentionCheckNotificationHook: c.sendHookNotification(elapsed, err) //nolint:errcheck } } } -func (c *RetentionCheck) sendEmailNotification(elapsed time.Duration, errCheck error) error { - body := new(bytes.Buffer) - data := make(map[string]any) - data["Results"] = c.results - totalDeletedFiles := 0 - totalDeletedSize := int64(0) - for _, result := range c.results { - totalDeletedFiles += result.DeletedFiles - totalDeletedSize += result.DeletedSize +func (c *RetentionCheck) sendEmailNotification(errCheck error) error { + params := EventParams{} + if len(c.results) > 0 || errCheck != nil { + params.retentionChecks = append(params.retentionChecks, executedRetentionCheck{ + Username: c.conn.User.Username, + ActionName: "Retention check", + Results: c.results, + }) } - data["HumanizeSize"] = util.ByteCountIEC - data["TotalFiles"] = totalDeletedFiles - data["TotalSize"] = totalDeletedSize - data["Elapsed"] = elapsed - data["Username"] = c.conn.User.Username - data["StartTime"] = util.GetTimeFromMsecSinceEpoch(c.StartTime) - if errCheck == nil { - data["Status"] = "Succeeded" - } else { - data["Status"] = "Failed" - } - if err := smtp.RenderRetentionReportTemplate(body, data); err != nil { - c.conn.Log(logger.LevelError, "unable to render retention check template: %v", err) + 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) return err } + f.Name = "retention-report.zip" + files = append(files, f) + startTime := time.Now() - subject := fmt.Sprintf("Retention check completed for user %#v", c.conn.User.Username) - if err := smtp.SendEmail([]string{c.Email}, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil { + var subject string + if errCheck == nil { + subject = fmt.Sprintf("Successful retention check for user %q", c.conn.User.Username) + } else { + subject = fmt.Sprintf("Retention check failed for user %q", c.conn.User.Username) + } + 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, time.Since(startTime)) return err diff --git a/internal/common/dataretention_test.go b/internal/common/dataretention_test.go index 8ee10b04..3b7dc9a3 100644 --- a/internal/common/dataretention_test.go +++ b/internal/common/dataretention_test.go @@ -146,21 +146,36 @@ func TestRetentionEmailNotifications(t *testing.T) { conn.ID = fmt.Sprintf("data_retention_%v", user.Username) check.conn = conn check.sendNotifications(1*time.Second, nil) - err = check.sendEmailNotification(1*time.Second, nil) + err = check.sendEmailNotification(nil) assert.NoError(t, err) - err = check.sendEmailNotification(1*time.Second, errors.New("test error")) + err = check.sendEmailNotification(errors.New("test error")) assert.NoError(t, err) + check.results = nil + err = check.sendEmailNotification(nil) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "no data retention report available") + } + smtpCfg.Port = 2626 err = smtpCfg.Initialize(configDir) require.NoError(t, err) - err = check.sendEmailNotification(1*time.Second, nil) + err = check.sendEmailNotification(nil) assert.Error(t, err) + check.results = []folderRetentionCheckResult{ + { + Path: "/", + Retention: 24, + DeletedFiles: 20, + DeletedSize: 456789, + Elapsed: 12 * time.Second, + }, + } smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir) require.NoError(t, err) - err = check.sendEmailNotification(1*time.Second, nil) + err = check.sendEmailNotification(nil) assert.Error(t, err) } diff --git a/internal/smtp/smtp.go b/internal/smtp/smtp.go index 617a102f..f99c2b85 100644 --- a/internal/smtp/smtp.go +++ b/internal/smtp/smtp.go @@ -43,9 +43,8 @@ const ( ) const ( - templateEmailDir = "email" - templateRetentionCheckResult = "retention-check-report.html" - templatePasswordReset = "reset-password.html" + templateEmailDir = "email" + templatePasswordReset = "reset-password.html" ) var ( @@ -156,24 +155,13 @@ func (c *Config) getAuthType() mail.AuthType { func loadTemplates(templatesPath string) { logger.Debug(logSender, "", "loading templates from %#v", templatesPath) - retentionCheckPath := filepath.Join(templatesPath, templateRetentionCheckResult) - retentionTmpl := util.LoadTemplate(nil, retentionCheckPath) passwordResetPath := filepath.Join(templatesPath, templatePasswordReset) pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath) - emailTemplates[templateRetentionCheckResult] = retentionTmpl emailTemplates[templatePasswordReset] = pwdResetTmpl } -// RenderRetentionReportTemplate executes the retention report template -func RenderRetentionReportTemplate(buf *bytes.Buffer, data any) error { - if smtpServer == nil { - return errors.New("smtp: not configured") - } - return emailTemplates[templateRetentionCheckResult].Execute(buf, data) -} - // RenderPasswordResetTemplate executes the password reset template func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error { if smtpServer == nil { diff --git a/pkgs/build.sh b/pkgs/build.sh index 1f7a948e..44ca30c7 100755 --- a/pkgs/build.sh +++ b/pkgs/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -NFPM_VERSION=2.18.1 +NFPM_VERSION=2.19.1 NFPM_ARCH=${NFPM_ARCH:-amd64} if [ -z ${SFTPGO_VERSION} ] then diff --git a/templates/email/retention-check-report.html b/templates/email/retention-check-report.html deleted file mode 100644 index a0aa5b68..00000000 --- a/templates/email/retention-check-report.html +++ /dev/null @@ -1,46 +0,0 @@ - -Retention check report for user "{{.Username}}" -

-Status: {{.Status}} -
-Start time: {{.StartTime}} -
-Total files deleted: {{.TotalFiles}} -
-Total size deleted: {{call .HumanizeSize .TotalSize}} -
-Elapsed: {{.Elapsed}} -
-{{range .Results -}} -

- Path: {{.Path}} - {{- if .Error}} -
- Error: {{.Error}} - {{- end}} - {{- if .Info}} -
- Info: {{.Info}} - {{- end}} -
- Retention: {{.Retention}} hours -
- Files deleted: {{.DeletedFiles}} -
- Size deleted: {{call $.HumanizeSize .DeletedSize}} -

-{{end}} \ No newline at end of file