From 8050fe0ea1aeead66d61025410b7e5269620a612 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Wed, 30 Aug 2023 01:17:27 +0200 Subject: [PATCH] added the ability to send mails via a cronjob --- server/backend/cron/cronjobs.go | 7 +- .../cron/emailTemplates/feedbackBody.gohtml | 43 ++++++++ .../cron/emailTemplates/feedbackBody.txt.tmpl | 20 ++++ server/backend/cron/feedbackEmail.go | 103 ++++++++++++++++++ server/backend/migration/20230826000000.go | 31 +++++- server/go.mod | 2 + server/go.sum | 4 + server/model/feedback.go | 1 + 8 files changed, 207 insertions(+), 4 deletions(-) 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 diff --git a/server/backend/cron/cronjobs.go b/server/backend/cron/cronjobs.go index d37dc1b4..596ed30f 100644 --- a/server/backend/cron/cronjobs.go +++ b/server/backend/cron/cronjobs.go @@ -28,7 +28,7 @@ const ( StorageDir = "/Storage/" // target location of files IOSNotifications = "iosNotifications" IOSActivityReset = "iosActivityReset" - + FeedbackEmail = "feedbackEmail" /* MensaType = "mensa" ChatType = "chat" KinoType = "kino" @@ -58,7 +58,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, @@ -67,6 +67,7 @@ func (c *CronService) Run() error { CanteenHeadcount, IOSNotifications, IOSActivityReset, + FeedbackEmail, ). Scan(&res) @@ -119,6 +120,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..c4a2fc1d --- /dev/null +++ b/server/backend/cron/emailTemplates/feedbackBody.gohtml @@ -0,0 +1,43 @@ +

Feedback via TumCampusApp:

+ +{{ if .Feedback.Valid }} +
+ {{ .Feedback.String }} +
+{{ 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..cb3f5820 --- /dev/null +++ b/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl @@ -0,0 +1,20 @@ +Feedback via TumCampusApp: + +{{- if .Feedback.Valid }} +{{ .Feedback.String }} +{{- end }} + +{{- if .Latitude.Valid }} +- Nutzer-Standort: + latitude: {{ .Latitude.Float64 }}, longitude: {{ .Longitude.Float64 }} + 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 }} +Fotos: + {{- range $val := Iterate .ImageCount }} +- Foto {{ $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..f52f539d --- /dev/null +++ b/server/backend/cron/feedbackEmail.go @@ -0,0 +1,103 @@ +package cron + +import ( + "bytes" + "crypto/tls" + "github.com/TUM-Dev/Campus-Backend/server/model" + log "github.com/sirupsen/logrus" + "gopkg.in/gomail.v2" + htmlTemplate "html/template" + "os" + "strconv" + textTemplate "text/template" + "time" +) +import _ "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 (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 + } + funcMap := textTemplate.FuncMap{"Iterate": Iterate} + parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody) + if err != nil { + log.WithError(err).Fatal("htmlFeedbackBody is not a valid template") + return err + } + parsedTxtBody, err := textTemplate.New("txtFeedbackBody").Funcs(funcMap).Parse(txtFeedbackBody) + if err != nil { + log.WithError(err).Fatal("txtFeedbackBody is not a valid template") + 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 := gomail.NewMessage() + // set message-headers + m.SetAddressHeader("From", os.Getenv("SMTP_USERNAME"), "TUM Campus App") + if feedback.Receiver.Valid { + m.SetHeader("To", feedback.Receiver.String) + } else { + m.SetHeader("To", "app@tum.de") + } + if feedback.ReplyTo.Valid { + m.SetHeader("Reply-To", feedback.ReplyTo.String) + } + if feedback.Timestamp.Valid { + m.SetDateHeader("Date", feedback.Timestamp.Time) + } else { + m.SetDateHeader("Date", time.Time{}) + } + m.SetHeader("Subject", "Feedback via Tum Campus App") + + // attach a body + var txtBodyBuffer bytes.Buffer + if err := parsedTxtBody.Execute(&txtBodyBuffer, feedback); err != nil { + return err + } + m.SetBody("text/plain", txtBodyBuffer.String()) + + var htmlBodyBuffer bytes.Buffer + if err := parsedHtmlBody.Execute(&htmlBodyBuffer, feedback); err != nil { + return err + } + m.AddAlternative("text/html", htmlBodyBuffer.String()) + + // send mail + if err := d.DialAndSend(m); err != nil { + log.WithError(err).Error("could not send mail") + continue + } + log.Trace("sending feedback %d to %s 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 index 48e129f3..5564d017 100644 --- a/server/backend/migration/20230826000000.go +++ b/server/backend/migration/20230826000000.go @@ -2,11 +2,14 @@ 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;"` } @@ -22,17 +25,41 @@ 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 } - return tx.Migrator().AddColumn(&Feedback{}, "AppVersion") + 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 } - return tx.Migrator().DropColumn(&Feedback{}, "AppVersion") + 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/go.mod b/server/go.mod index 9840a7ee..4f8fb2fc 100644 --- a/server/go.mod +++ b/server/go.mod @@ -23,6 +23,7 @@ require ( google.golang.org/grpc v1.57.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.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 ) @@ -52,5 +53,6 @@ require ( golang.org/x/text v0.12.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 78d7ab2d..c6023e01 100644 --- a/server/go.sum +++ b/server/go.sum @@ -192,9 +192,13 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/model/feedback.go b/server/model/feedback.go index ee95b09c..83d877a1 100644 --- a/server/model/feedback.go +++ b/server/model/feedback.go @@ -15,6 +15,7 @@ type Feedback struct { Longitude sql.NullFloat64 `gorm:"column:longitude;type:float;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;"` Timestamp sql.NullTime `gorm:"column:timestamp;type:timestamp;default:CURRENT_TIMESTAMP;null;"` }