From dcbeaf22295e45d19755ab4e59f092ef40eecbf7 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 16 Sep 2023 16:09:44 +0200 Subject: [PATCH 1/9] added a feedback cronjob --- .env | 5 + docker-compose.yaml | 4 + server/backend/cron/cronjobs.go | 6 +- .../cron/emailTemplates/feedbackBody.gohtml | 42 ++++++ .../cron/emailTemplates/feedbackBody.txt.tmpl | 22 +++ server/backend/cron/feedbackEmail.go | 139 ++++++++++++++++++ server/backend/migration/20230826000000.go | 66 +++++++++ server/backend/migration/migration.go | 1 + server/go.mod | 2 + server/go.sum | 4 + server/model/feedback.go | 25 ++++ 11 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 server/backend/cron/emailTemplates/feedbackBody.gohtml create mode 100644 server/backend/cron/emailTemplates/feedbackBody.txt.tmpl create mode 100644 server/backend/cron/feedbackEmail.go create mode 100644 server/backend/migration/20230826000000.go create mode 100644 server/model/feedback.go diff --git a/.env b/.env index e8387199..132f9c97 100644 --- a/.env +++ b/.env @@ -10,3 +10,8 @@ APNS_P8_FILE_PATH=/secrets/AuthKey_XXXX.p8 SENTRY_DSN= CAMPUS_API_TOKEN= + +SMTP_PASSWORD= +SMTP_URL= +SMTP_USERNAME= +SMTP_PORT= diff --git a/docker-compose.yaml b/docker-compose.yaml index 2efbe878..fd23ca8d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,6 +19,10 @@ services: - MensaCronDisabled=true - OMDB_API_KEY=${OMDB_API_KEY} - CAMPUS_API_TOKEN=${CAMPUS_API_TOKEN} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SMTP_URL=${SMTP_URL:-postout.lrz.de} + - SMTP_USERNAME=${SMTP_USERNAME:-tca-support.os.in@tum.de} + - SMTP_PORT=${SMTP_PORT:-587} volumes: - backend-storage:/Storage - ./apns_auth_key.p8:${APNS_P8_FILE_PATH} diff --git a/server/backend/cron/cronjobs.go b/server/backend/cron/cronjobs.go index 396ca328..55c8bce0 100644 --- a/server/backend/cron/cronjobs.go +++ b/server/backend/cron/cronjobs.go @@ -32,6 +32,7 @@ const ( IOSActivityReset = "iosActivityReset" NewExamResultsHook = "newExamResultsHook" MovieType = "movie" + FeedbackEmail = "feedbackEmail" /* MensaType = "mensa" AlarmType = "alarm" */ @@ -59,7 +60,7 @@ func (c *CronService) Run() error { var res []model.Crontab c.db.Model(&model.Crontab{}). - Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?)", + Where("`interval` > 0 AND (lastRun+`interval`) < ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", time.Now().Unix(), NewsType, FileDownloadType, @@ -70,6 +71,7 @@ func (c *CronService) Run() error { IOSActivityReset, NewExamResultsHook, MovieType, + FeedbackEmail, ). Scan(&res) @@ -125,6 +127,8 @@ func (c *CronService) Run() error { g.Go(func() error { return c.iosNotificationsCron() }) case IOSActivityReset: g.Go(func() error { return c.iosActivityReset() }) + case FeedbackEmail: + g.Go(func() error { return c.feedbackEmailCron() }) } } diff --git a/server/backend/cron/emailTemplates/feedbackBody.gohtml b/server/backend/cron/emailTemplates/feedbackBody.gohtml new file mode 100644 index 00000000..411eb53e --- /dev/null +++ b/server/backend/cron/emailTemplates/feedbackBody.gohtml @@ -0,0 +1,42 @@ +

Feedback via TumCampusApp:

+{{ if .Feedback.Valid -}} +
+ {{- .Feedback.String -}} +
+{{- else -}} +no feedback provided +{{- end }} + + + + + + {{- if .Latitude.Valid }} + + + + + {{- end }} + + + + + + + + +
Inforation typeDetails
Nutzer-Standort + + latitude: {{ .Latitude.Float64 }}, longitude: {{ .Longitude.Float64 }} + +
OS-Version{{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}}
App-Version{{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}}
+{{- if .ImageCount }} +

Fotos:


+
    +{{- range $val := iterate .ImageCount }} +
  1. + Foto {{ $val }} +
  2. +{{- end }} +
+{{- end -}} diff --git a/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl b/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl new file mode 100644 index 00000000..e4c6c7d0 --- /dev/null +++ b/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl @@ -0,0 +1,22 @@ +Feedback via TumCampusApp: + +{{ if .Feedback.Valid }} +{{- .Feedback.String -}} +{{ else -}} +no feedback provided +{{- end }} + +Metadata: +{{- if .Latitude.Valid }} +- Nutzer-Standort: {{ .Latitude.Float64 }},{{ .Longitude.Float64 }} (latitude,longitude) + https://www.google.com/maps/search/?api=1&query={{ .Latitude.Float64 }},{{ .Longitude.Float64 }} +{{- end }} +- OS-Version: {{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}} +- App-Version: {{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}} +{{- if .ImageCount }} + +Photos: + {{- range $val := iterate .ImageCount }} +- Photo {{ $val }}: https://app.tum.de/File/feedback/{{ $.Id }}/{{ $val }}.png + {{- end -}} +{{- end -}} diff --git a/server/backend/cron/feedbackEmail.go b/server/backend/cron/feedbackEmail.go new file mode 100644 index 00000000..2deab84f --- /dev/null +++ b/server/backend/cron/feedbackEmail.go @@ -0,0 +1,139 @@ +package cron + +import ( + "bytes" + "crypto/tls" + htmlTemplate "html/template" + "os" + "strconv" + textTemplate "text/template" + "time" + + "github.com/TUM-Dev/Campus-Backend/server/model" + log "github.com/sirupsen/logrus" + "gopkg.in/gomail.v2" + + _ "embed" +) + +// iterate is necessary, as go otherwise cannot count up in a for loop inside templates +func iterate(count int32) []int32 { + var items []int32 + var i int32 + for i = 0; i < count; i++ { + items = append(items, i) + } + return items +} + +//go:embed emailTemplates/feedbackBody.gohtml +var htmlFeedbackBody string + +//go:embed emailTemplates/feedbackBody.txt.tmpl +var txtFeedbackBody string + +func parseTemplates() (*htmlTemplate.Template, *textTemplate.Template, error) { + funcMap := textTemplate.FuncMap{"iterate": iterate} + parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody) + if err != nil { + return nil, nil, err + } + parsedTxtBody, err := textTemplate.New("txtFeedbackBody").Funcs(funcMap).Parse(txtFeedbackBody) + if err != nil { + return nil, nil, err + } + return parsedHtmlBody, parsedTxtBody, nil + +} + +type MailHeaders struct { + From string + To string + ReplyTo string //optional + Timestamp time.Time + Subject string +} + +func messageWithHeaders(feedback *model.Feedback) *gomail.Message { + m := gomail.NewMessage() + // From + m.SetAddressHeader("From", os.Getenv("SMTP_USERNAME"), "TUM Campus App") + // To + if feedback.Receiver.Valid { + m.SetHeader("To", feedback.Receiver.String) + } else { + m.SetHeader("To", "app@tum.de") + } + // ReplyTo + if feedback.ReplyTo.Valid { + m.SetHeader("Reply-To", feedback.ReplyTo.String) + } + // Timestamp + if feedback.Timestamp.Valid { + m.SetDateHeader("Date", feedback.Timestamp.Time) + } else { + m.SetDateHeader("Date", time.Now()) + } + // Subject + m.SetHeader("Subject", "Feedback via Tum Campus App") + return m +} + +func generateTemplatedMail(parsedHtmlBody *htmlTemplate.Template, parsedTxtBody *textTemplate.Template, feedback *model.Feedback) (string, string, error) { + var htmlBodyBuffer bytes.Buffer + if err := parsedHtmlBody.Execute(&htmlBodyBuffer, feedback); err != nil { + return "", "", err + } + var txtBodyBuffer bytes.Buffer + if err := parsedTxtBody.Execute(&txtBodyBuffer, feedback); err != nil { + return "", "", err + } + return htmlBodyBuffer.String(), txtBodyBuffer.String(), nil +} + +func (c *CronService) feedbackEmailCron() error { + + var results []model.Feedback + if err := c.db.Find(&results, "processed = false").Scan(&results).Error; err != nil { + log.WithError(err).Fatal("could not get unprocessed feedback") + return err + } + parsedHtmlBody, parsedTxtBody, err := parseTemplates() + if err != nil { + log.WithError(err).Fatal("could not parse email templates") + return err + } + + smtpPort, err := strconv.Atoi(os.Getenv("SMTP_PORT")) + if err != nil { + log.WithError(err).Fatal("SMTP_PORT is not an integer") + return err + } + d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD")) + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + for i, feedback := range results { + m := messageWithHeaders(&feedback) + + // attach a body + htmlBodyBuffer, txtBodyBuffer, err := generateTemplatedMail(parsedHtmlBody, parsedTxtBody, &feedback) + if err != nil { + log.WithError(err).Error("Could not template mail body") + return err + } + m.SetBody("text/plain", txtBodyBuffer) + m.AddAlternative("text/html", htmlBodyBuffer) + + // send mail + if err := d.DialAndSend(m); err != nil { + log.WithError(err).Error("could not send mail") + continue + } + log.Tracef("sending feedback %d to %v successfull", i, feedback.Receiver) + + // prevent the message being send the next time around + if err := c.db.Find(model.Feedback{}, "id = ?", feedback.Id).Update("processed", "true").Error; err != nil { + log.WithError(err).Error("could not prevent mail from being send again") + } + } + return nil +} diff --git a/server/backend/migration/20230826000000.go b/server/backend/migration/20230826000000.go new file mode 100644 index 00000000..505e3f28 --- /dev/null +++ b/server/backend/migration/20230826000000.go @@ -0,0 +1,66 @@ +package migration + +import ( + "database/sql" + + "github.com/TUM-Dev/Campus-Backend/server/model" + "github.com/go-gormigrate/gormigrate/v2" + "github.com/guregu/null" + "gorm.io/gorm" +) + +type Feedback struct { + Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"` + OsVersion sql.NullString `gorm:"column:os_version;type:text;null;"` + AppVersion sql.NullString `gorm:"column:app_version;type:text;null;"` +} + +// TableName sets the insert table name for this struct type +func (n *Feedback) TableName() string { + return "feedback" +} + +// migrate20230826000000 +// adds a "feedbackEmail" cron job that runs every 30 minutes. +func (m TumDBMigrator) migrate20230826000000() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "20230826000000", + Migrate: func(tx *gorm.DB) error { + if err := tx.Migrator().AddColumn(&Feedback{}, "Processed"); err != nil { + return err + } + if err := tx.Migrator().AddColumn(&Feedback{}, "OsVersion"); err != nil { + return err + } + if err := tx.Migrator().AddColumn(&Feedback{}, "AppVersion"); err != nil { + return err + } + if err := tx.Exec("UPDATE feedback SET processed = true WHERE processed != true;").Error; err != nil { + return err + } + if err := SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail"); err != nil { + return err + } + return tx.Create(&model.Crontab{ + Interval: 60 * 30, // Every 30 minutes + Type: null.String{NullString: sql.NullString{String: "feedbackEmail", Valid: true}}, + }).Error + }, + + Rollback: func(tx *gorm.DB) error { + if err := tx.Migrator().DropColumn(&Feedback{}, "Processed"); err != nil { + return err + } + if err := tx.Migrator().DropColumn(&Feedback{}, "OsVersion"); err != nil { + return err + } + if err := tx.Migrator().DropColumn(&Feedback{}, "AppVersion"); err != nil { + return err + } + if err := tx.Delete(&model.Crontab{}, "type = ? AND interval = ?", "fileDownload", 30*60).Error; err != nil { + return err + } + return SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail") + }, + } +} diff --git a/server/backend/migration/migration.go b/server/backend/migration/migration.go index b5dcbcf5..d975f58c 100644 --- a/server/backend/migration/migration.go +++ b/server/backend/migration/migration.go @@ -52,6 +52,7 @@ func (m TumDBMigrator) Migrate() error { m.migrate20230904000000(), m.migrate20230530000000(), m.migrate20230904100000(), + m.migrate20230826000000(), }) err := mig.Migrate() return err diff --git a/server/go.mod b/server/go.mod index e911c766..dd4505ef 100644 --- a/server/go.mod +++ b/server/go.mod @@ -26,6 +26,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb google.golang.org/grpc v1.58.0 google.golang.org/protobuf v1.31.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/mysql v1.5.1 gorm.io/gorm v1.25.4 ) @@ -55,5 +56,6 @@ require ( golang.org/x/text v0.13.0 // indirect google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index 4c8afde7..0a8951dd 100644 --- a/server/go.sum +++ b/server/go.sum @@ -902,6 +902,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -910,6 +912,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/server/model/feedback.go b/server/model/feedback.go new file mode 100644 index 00000000..6a0ea3d1 --- /dev/null +++ b/server/model/feedback.go @@ -0,0 +1,25 @@ +package model + +import ( + "github.com/guregu/null" +) + +type Feedback struct { + Id int64 `gorm:"column:id;primary_key;AUTO_INCREMENT;type:int;not null;"` + ImageCount int32 `gorm:"column:image_count;type:int;not null;"` + EmailId null.String `gorm:"column:email_id;type:text;null"` + Receiver null.String `gorm:"column:receiver;type:text;null"` + ReplyTo null.String `gorm:"column:reply_to;type:text;null"` + Feedback null.String `gorm:"column:feedback;type:text;null"` + Latitude null.Float `gorm:"column:latitude;type:float;null;"` + Longitude null.Float `gorm:"column:longitude;type:float;null;"` + OsVersion null.String `gorm:"column:os_version;type:text;null;"` + AppVersion null.String `gorm:"column:app_version;type:text;null;"` + Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"` + Timestamp null.Time `gorm:"column:timestamp;type:timestamp;default:CURRENT_TIMESTAMP;null;"` +} + +// TableName sets the insert table name for this struct type +func (n *Feedback) TableName() string { + return "feedback" +} From a740c9505145ebf816bb95152d719416e9894b65 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 16 Sep 2023 16:10:17 +0200 Subject: [PATCH 2/9] added a testcase --- server/backend/cron/feedbackEmail_test.go | 156 ++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 server/backend/cron/feedbackEmail_test.go diff --git a/server/backend/cron/feedbackEmail_test.go b/server/backend/cron/feedbackEmail_test.go new file mode 100644 index 00000000..2ad22eae --- /dev/null +++ b/server/backend/cron/feedbackEmail_test.go @@ -0,0 +1,156 @@ +package cron + +import ( + "github.com/guregu/null" + "os" + "testing" + "time" + + "github.com/TUM-Dev/Campus-Backend/server/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIterate(t *testing.T) { + assert.Equal(t, []int32(nil), iterate(0)) + assert.Equal(t, []int32{0}, iterate(1)) + assert.Equal(t, []int32{0, 1}, iterate(2)) + assert.Equal(t, []int32{0, 1, 2}, iterate(3)) + assert.Equal(t, []int32{0, 1, 2, 3}, iterate(4)) + assert.Equal(t, 42, len(iterate(42))) +} + +func fullFeedback() *model.Feedback { + return &model.Feedback{ + EmailId: null.StringFrom("magic-id"), + Receiver: null.StringFrom("tca"), + ReplyTo: null.StringFrom("test@example.de"), + Feedback: null.StringFrom("This is a Test"), + ImageCount: 1, + Latitude: null.FloatFrom(0), + Longitude: null.FloatFrom(0), + AppVersion: null.StringFrom("TCA 10.2"), + OsVersion: null.StringFrom("Android 10.0"), + Timestamp: null.TimeFrom(time.Now()), + } +} + +func emptyFeedback() *model.Feedback { + return &model.Feedback{ + EmailId: null.String{}, + Receiver: null.String{}, + ReplyTo: null.String{}, + Feedback: null.String{}, + ImageCount: 0, + Latitude: null.Float{}, + Longitude: null.Float{}, + AppVersion: null.String{}, + OsVersion: null.String{}, + Timestamp: null.Time{}, + } +} + +func TestHeaderInstantiationWithFullFeedback(t *testing.T) { + require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de")) + fb := fullFeedback() + m := messageWithHeaders(fb) + assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From")) + assert.Equal(t, []string{fb.Receiver.String}, m.GetHeader("To")) + assert.Equal(t, []string{"test@example.de"}, m.GetHeader("Reply-To")) + assert.Equal(t, []string{fb.Timestamp.Time.Format(time.RFC1123Z)}, m.GetHeader("Date")) + assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject")) +} + +func TestHeaderInstantiationWithEmptyFeedback(t *testing.T) { + require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de")) + m := messageWithHeaders(emptyFeedback()) + assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From")) + assert.Equal(t, []string{"app@tum.de"}, m.GetHeader("To")) + assert.Equal(t, []string(nil), m.GetHeader("Reply-To")) + // Date is set to now in messageWithHeaders => checking that this is actually now is a bit tricker + dates := m.GetHeader("Date") + assert.Equal(t, 1, len(dates)) + date, err := time.Parse(time.RFC1123Z, dates[0]) + require.NoError(t, err) + assert.WithinDuration(t, time.Now(), date, time.Minute) + assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject")) +} + +func TestTemplatingResultsWithFullFeedback(t *testing.T) { + html, txt, err := parseTemplates() + require.NoError(t, err) + htmlBody, txtBody, err := generateTemplatedMail(html, txt, fullFeedback()) + require.NoError(t, err) + assert.Equal(t, `

Feedback via TumCampusApp:

+
This is a Test
+ + + + + + + + + + + + + + + + + +
Inforation typeDetails
Nutzer-Standort + + latitude: 0, longitude: 0 + +
OS-VersionAndroid 10.0
App-VersionTCA 10.2
+

Fotos:


+
    +
  1. + Foto 0 +
  2. +
`, htmlBody) + assert.Equal(t, `Feedback via TumCampusApp: + +This is a Test + +Metadata: +- Nutzer-Standort: 0,0 (latitude,longitude) + https://www.google.com/maps/search/?api=1&query=0,0 +- OS-Version: Android 10.0 +- App-Version: TCA 10.2 + +Photos: +- Photo 0: https://app.tum.de/File/feedback/0/0.png`, txtBody) +} + +func TestTemplatingResultsWithEmptyFeedback(t *testing.T) { + html, txt, err := parseTemplates() + require.NoError(t, err) + htmlBody, txtBody, err := generateTemplatedMail(html, txt, emptyFeedback()) + require.NoError(t, err) + assert.Equal(t, `

Feedback via TumCampusApp:

+no feedback provided + + + + + + + + + + + + + +
Inforation typeDetails
OS-Versionunknown
App-Versionunknown
`, htmlBody) + assert.Equal(t, `Feedback via TumCampusApp: + +no feedback provided + +Metadata: +- OS-Version: unknown +- App-Version: unknown`, txtBody) +} From dcf7403c33a3ba7fff777a69ce18ae553203af6c Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 23 Sep 2023 02:18:31 +0200 Subject: [PATCH 3/9] linting fixes --- client/go.mod | 2 +- client/go.sum | 4 ++-- server/backend/cron/feedbackEmail_test.go | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/go.mod b/client/go.mod index a03263a5..d02d8050 100644 --- a/client/go.mod +++ b/client/go.mod @@ -3,7 +3,7 @@ module github.com/TUM-Dev/Campus-Backend/client go 1.21 require ( - github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922140200-2092d9ee26fb + github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922221907-9ec2b923010a github.com/sirupsen/logrus v1.9.3 google.golang.org/grpc v1.58.1 ) diff --git a/client/go.sum b/client/go.sum index 9d4eb14e..2abd1ac8 100644 --- a/client/go.sum +++ b/client/go.sum @@ -1,5 +1,5 @@ -github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922140200-2092d9ee26fb h1:zCxXbWPCo1z6+CadxETHfjcGWlgQQN55S3voe2PLXsQ= -github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922140200-2092d9ee26fb/go.mod h1:FIIdW5aglREN0ULXZXDQtvuBdbTMa/fCrKSTS8FYP7k= +github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922221907-9ec2b923010a h1:t7ahAwtri1UztDL6t0kce0OOkblQVHdMIWrhHkuEelQ= +github.com/TUM-Dev/Campus-Backend/server v0.0.0-20230922221907-9ec2b923010a/go.mod h1:FIIdW5aglREN0ULXZXDQtvuBdbTMa/fCrKSTS8FYP7k= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/server/backend/cron/feedbackEmail_test.go b/server/backend/cron/feedbackEmail_test.go index 2ad22eae..7c5f1a2e 100644 --- a/server/backend/cron/feedbackEmail_test.go +++ b/server/backend/cron/feedbackEmail_test.go @@ -1,11 +1,12 @@ package cron import ( - "github.com/guregu/null" "os" "testing" "time" + "github.com/guregu/null" + "github.com/TUM-Dev/Campus-Backend/server/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" From 2d2826f8ee1043a058347463c5315a16f4ea1272 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 23 Sep 2023 02:24:08 +0200 Subject: [PATCH 4/9] fixed naming issues --- .../feedback_body.gohtml} | 0 .../feedback_body.txt.tmpl} | 0 server/backend/cron/{feedbackEmail.go => feedback_email.go} | 4 ++-- .../cron/{feedbackEmail_test.go => feedback_email_test.go} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename server/backend/cron/{emailTemplates/feedbackBody.gohtml => email_templates/feedback_body.gohtml} (100%) rename server/backend/cron/{emailTemplates/feedbackBody.txt.tmpl => email_templates/feedback_body.txt.tmpl} (100%) rename server/backend/cron/{feedbackEmail.go => feedback_email.go} (97%) rename server/backend/cron/{feedbackEmail_test.go => feedback_email_test.go} (100%) diff --git a/server/backend/cron/emailTemplates/feedbackBody.gohtml b/server/backend/cron/email_templates/feedback_body.gohtml similarity index 100% rename from server/backend/cron/emailTemplates/feedbackBody.gohtml rename to server/backend/cron/email_templates/feedback_body.gohtml diff --git a/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl b/server/backend/cron/email_templates/feedback_body.txt.tmpl similarity index 100% rename from server/backend/cron/emailTemplates/feedbackBody.txt.tmpl rename to server/backend/cron/email_templates/feedback_body.txt.tmpl diff --git a/server/backend/cron/feedbackEmail.go b/server/backend/cron/feedback_email.go similarity index 97% rename from server/backend/cron/feedbackEmail.go rename to server/backend/cron/feedback_email.go index 2deab84f..2cef8a9f 100644 --- a/server/backend/cron/feedbackEmail.go +++ b/server/backend/cron/feedback_email.go @@ -26,10 +26,10 @@ func iterate(count int32) []int32 { return items } -//go:embed emailTemplates/feedbackBody.gohtml +//go:embed email_templates/feedback_body.gohtml var htmlFeedbackBody string -//go:embed emailTemplates/feedbackBody.txt.tmpl +//go:embed email_templates/feedback_body.txt.tmpl var txtFeedbackBody string func parseTemplates() (*htmlTemplate.Template, *textTemplate.Template, error) { diff --git a/server/backend/cron/feedbackEmail_test.go b/server/backend/cron/feedback_email_test.go similarity index 100% rename from server/backend/cron/feedbackEmail_test.go rename to server/backend/cron/feedback_email_test.go From cceb25498fccd95f7f55620859bd0b71dcd4108a Mon Sep 17 00:00:00 2001 From: Kordian Bruck Date: Sun, 1 Oct 2023 22:38:48 +0200 Subject: [PATCH 5/9] Change comment --- server/backend/cron/feedback_email.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/backend/cron/feedback_email.go b/server/backend/cron/feedback_email.go index 2cef8a9f..3afe617b 100644 --- a/server/backend/cron/feedback_email.go +++ b/server/backend/cron/feedback_email.go @@ -16,7 +16,7 @@ import ( _ "embed" ) -// iterate is necessary, as go otherwise cannot count up in a for loop inside templates +// iterate is a template helper to make counting possible func iterate(count int32) []int32 { var items []int32 var i int32 From 58bbc28935c137685cfcd4eeda3915dde301100d Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 2 Oct 2023 08:49:53 +0200 Subject: [PATCH 6/9] fixd not using the null shorthands --- server/backend/migration/20230826000000.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/backend/migration/20230826000000.go b/server/backend/migration/20230826000000.go index 505e3f28..c6dc3768 100644 --- a/server/backend/migration/20230826000000.go +++ b/server/backend/migration/20230826000000.go @@ -1,8 +1,6 @@ package migration import ( - "database/sql" - "github.com/TUM-Dev/Campus-Backend/server/model" "github.com/go-gormigrate/gormigrate/v2" "github.com/guregu/null" @@ -10,9 +8,9 @@ import ( ) type Feedback struct { - Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"` - OsVersion sql.NullString `gorm:"column:os_version;type:text;null;"` - AppVersion sql.NullString `gorm:"column:app_version;type:text;null;"` + Processed bool `gorm:"column:processed;type:boolean;default:false;not null;"` + OsVersion null.String `gorm:"column:os_version;type:text;null;"` + AppVersion null.String `gorm:"column:app_version;type:text;null;"` } // TableName sets the insert table name for this struct type @@ -43,7 +41,7 @@ func (m TumDBMigrator) migrate20230826000000() *gormigrate.Migration { } return tx.Create(&model.Crontab{ Interval: 60 * 30, // Every 30 minutes - Type: null.String{NullString: sql.NullString{String: "feedbackEmail", Valid: true}}, + Type: null.StringFrom("feedbackEmail"), }).Error }, From 60d99adb3027aba39c186884a8e0dfb963f26789 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 2 Oct 2023 09:06:22 +0200 Subject: [PATCH 7/9] fixed using interval lenght in migrations --- server/backend/migration/20230826000000.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/backend/migration/20230826000000.go b/server/backend/migration/20230826000000.go index c6dc3768..c3ec81de 100644 --- a/server/backend/migration/20230826000000.go +++ b/server/backend/migration/20230826000000.go @@ -55,7 +55,7 @@ func (m TumDBMigrator) migrate20230826000000() *gormigrate.Migration { if err := tx.Migrator().DropColumn(&Feedback{}, "AppVersion"); err != nil { return err } - if err := tx.Delete(&model.Crontab{}, "type = ? AND interval = ?", "fileDownload", 30*60).Error; err != nil { + if err := tx.Delete(&model.Crontab{Type: null.StringFrom("fileDownload")}).Error; err != nil { return err } return SafeEnumMigrate(tx, &model.Crontab{}, "type", "feedbackEmail") From 77fbd99295750d921ad624824807de17275ebd87 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 2 Oct 2023 17:35:37 +0200 Subject: [PATCH 8/9] moved embedded variables to the top of the file --- server/backend/cron/feedback_email.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/backend/cron/feedback_email.go b/server/backend/cron/feedback_email.go index 3afe617b..5dc6f667 100644 --- a/server/backend/cron/feedback_email.go +++ b/server/backend/cron/feedback_email.go @@ -16,6 +16,12 @@ import ( _ "embed" ) +//go:embed email_templates/feedback_body.gohtml +var htmlFeedbackBody string + +//go:embed email_templates/feedback_body.txt.tmpl +var txtFeedbackBody string + // iterate is a template helper to make counting possible func iterate(count int32) []int32 { var items []int32 @@ -26,12 +32,6 @@ func iterate(count int32) []int32 { return items } -//go:embed email_templates/feedback_body.gohtml -var htmlFeedbackBody string - -//go:embed email_templates/feedback_body.txt.tmpl -var txtFeedbackBody string - func parseTemplates() (*htmlTemplate.Template, *textTemplate.Template, error) { funcMap := textTemplate.FuncMap{"iterate": iterate} parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody) From 1ee7e96074b5dae5252f875481d205b0acb26397 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 2 Oct 2023 17:39:22 +0200 Subject: [PATCH 9/9] extracted the smtpdialer to its own function --- server/backend/cron/feedback_email.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/server/backend/cron/feedback_email.go b/server/backend/cron/feedback_email.go index 5dc6f667..01d39867 100644 --- a/server/backend/cron/feedback_email.go +++ b/server/backend/cron/feedback_email.go @@ -2,7 +2,6 @@ package cron import ( "bytes" - "crypto/tls" htmlTemplate "html/template" "os" "strconv" @@ -104,13 +103,10 @@ func (c *CronService) feedbackEmailCron() error { return err } - smtpPort, err := strconv.Atoi(os.Getenv("SMTP_PORT")) + dialer, err := setupSMTPDialer() if err != nil { - log.WithError(err).Fatal("SMTP_PORT is not an integer") return err } - d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD")) - d.TLSConfig = &tls.Config{InsecureSkipVerify: true} for i, feedback := range results { m := messageWithHeaders(&feedback) @@ -124,11 +120,11 @@ func (c *CronService) feedbackEmailCron() error { m.AddAlternative("text/html", htmlBodyBuffer) // send mail - if err := d.DialAndSend(m); err != nil { + if err := dialer.DialAndSend(m); err != nil { log.WithError(err).Error("could not send mail") continue } - log.Tracef("sending feedback %d to %v successfull", i, feedback.Receiver) + log.Tracef("sending feedback %dialer to %v successfull", i, feedback.Receiver) // prevent the message being send the next time around if err := c.db.Find(model.Feedback{}, "id = ?", feedback.Id).Update("processed", "true").Error; err != nil { @@ -137,3 +133,14 @@ func (c *CronService) feedbackEmailCron() error { } return nil } + +// setupSMTPDialer sets up the SMTP dialer +func setupSMTPDialer() (*gomail.Dialer, error) { + smtpPort, err := strconv.Atoi(os.Getenv("SMTP_PORT")) + if err != nil { + log.WithError(err).Fatal("SMTP_PORT is not an integer") + return nil, err + } + d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD")) + return d, nil +}