From b3290c357d7d60033a6669002d83267095a140d3 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 11 Jun 2025 17:29:14 +0200 Subject: [PATCH 1/8] Update to go 1.24, small code cleanup --- go.mod | 2 +- mail.go | 2 +- notice.go | 10 ---------- persistent_data.go | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index f31f7cb..acbf5d6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/ChaoticByte/wid-notifier -go 1.22 +go 1.24 diff --git a/mail.go b/mail.go index e4c5890..07477ba 100644 --- a/mail.go +++ b/mail.go @@ -66,7 +66,7 @@ func (r Recipient) sendNotices(notices []WidNotice, template MailTemplate, auth logger.error("Could not create mail from template") logger.error(err) } - // serialize & send mail + // serialize mail data = mailContent.serializeValidMail(smtpConfig.From, r.Address) // add to cache (*cache)[n.Uuid] = data diff --git a/notice.go b/notice.go index 8967d20..758d85d 100644 --- a/notice.go +++ b/notice.go @@ -25,16 +25,6 @@ type WidNotice struct { PortalUrl string } -// func (n WidNotice) serialized() ([]byte, error) { -// return json.Marshal(n) -// } - -// func NewWidNoticeFromJSON(data []byte) (WidNotice, error) { -// n := WidNotice{} -// err := json.Unmarshal(data, &n) -// return n, err -// } - func noticeSliceContains(notices []WidNotice, notice WidNotice) bool { for _, x := range notices { if x.Uuid == notice.Uuid { diff --git a/persistent_data.go b/persistent_data.go index 182c269..f3ce9ce 100644 --- a/persistent_data.go +++ b/persistent_data.go @@ -18,4 +18,4 @@ func NewPersistentData(c Config) PersistentData { d.LastPublished[e.Id] = time.Now().Add(-time.Hour * 24) // a day ago } return d -} \ No newline at end of file +} From 2e1aee0313884d0edb573a6991a44a8e93a0cabe Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 11 Jun 2025 17:45:14 +0200 Subject: [PATCH 2/8] Parse -h, --help and --version and show help, version --- main.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 47ce30b..e2fea3a 100644 --- a/main.go +++ b/main.go @@ -10,19 +10,39 @@ import ( "time" ) +var executableName string var logger Logger + +func showVersion() { + fmt.Printf("wid-notifier %s\n", Version) +} + +func showHelp() { + fmt.Printf("Usage: %v \n\nIf the config file doesn't exist, an incomplete \n" + + "configuration with default values is created.\n\n", + executableName) + showVersion() +} + func main() { // get cli arguments args := os.Args + executableName = args[0] if len(args) < 2 { - fmt.Printf( "Usage: %v \n\nIf the config file doesn't exist, an incomplete \n" + - "configuration with default values is created.\n\nVersion: %s\n", - args[0], - Version) + showHelp() os.Exit(1) } - configFilePath := os.Args[1] + for _, arg := range args { + if arg == "-h" || arg == "--help" { + showHelp() + os.Exit(0) + } else if arg == "--version" { + showVersion() + os.Exit(0) + } + } + configFilePath := args[1] // create logger logger = NewLogger(2) // init From ef24503214c05554973af86de38836207c5a7986 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 11 Jun 2025 22:05:44 +0200 Subject: [PATCH 3/8] fix: don't save last published timestamp when email sending fails, so they can be sent in the next iteration --- main.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index e2fea3a..62de84a 100644 --- a/main.go +++ b/main.go @@ -97,7 +97,8 @@ func main() { for { t1 := time.Now().UnixMilli() newNotices := []WidNotice{} - cache := map[string][]byte{} + lastPublished := map[string]time.Time{} // endpoint id : last published timestamp + cache := map[string][]byte{} // cache generated emails for reuse for _, a := range enabledApiEndpoints { logger.info("Querying endpoint '" + a.Id + "' for new notices ...") n, t, err := a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id]) @@ -113,14 +114,14 @@ func main() { logger.error(err) } else if len(n) > 0 { newNotices = append(newNotices, n...) - persistent.data.(PersistentData).LastPublished[a.Id] = t - persistent.save() + lastPublished[a.Id] = t } } logger.debug(fmt.Sprintf("Got %v new notices", len(newNotices))) if len(newNotices) > 0 { logger.info("Sending email notifications ...") recipientsNotified := 0 + var err error for _, r := range config.Recipients { // Filter notices for this recipient filteredNotices := []WidNotice{} @@ -134,14 +135,22 @@ func main() { slices.Reverse(filteredNotices) logger.debug(fmt.Sprintf("Including %v of %v notices for recipient %v", len(filteredNotices), len(newNotices), r.Address)) // Send notices - err := r.sendNotices(filteredNotices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache) + err = r.sendNotices(filteredNotices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache) if err != nil { logger.error(err) } else { recipientsNotified++ } } - logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(config.Recipients))) + if recipientsNotified < 1 && err != nil { + logger.error("Couldn't send any mail notification!") + } else { + for id, t := range lastPublished { + persistent.data.(PersistentData).LastPublished[id] = t + persistent.save() + } + logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(config.Recipients))) + } } dt := int(time.Now().UnixMilli() - t1) time.Sleep(time.Millisecond * time.Duration((config.ApiFetchInterval * 1000) - dt)) From 295ceec3de248a8f77f35c6113d43831569dc38a Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 12 Jun 2025 00:00:52 +0200 Subject: [PATCH 4/8] Improve config file structure by combining multiple recipients into lists (breaking) --- README.md | 21 +++++++++++---------- config.go | 36 +++++++++++++++++++++--------------- mail.go | 21 +++++++++++---------- main.go | 35 ++++++++++++++++++++++++----------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 1c6ddbd..e04f337 100644 --- a/README.md +++ b/README.md @@ -34,27 +34,28 @@ Example: ```json { "api_fetch_interval": 600, - "datafile": "data.json", "enabled_api_endpoints": [ "bay", "bund" ], + "datafile": "data.json", "loglevel": 2, - "recipients": [ + "lists": [ { - "address": "guenther@example.org", - "include": [ - {"classification": "kritisch"}, - {"title_contains": "jQuery"} + "name": "Example List", + "recipients": ["someone@example.org"], + "filter": [ + {"classification": "hoch", "title_contains": "Microsoft"}, + {"classification": "kritisch"} ] } ], "smtp": { - "from": "from@example.org", - "host": "example.org", + "from": "user@localhost", + "host": "127.0.0.1", "port": 587, - "user": "from@example.org", - "password": "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDüRfEnSiEnIcHt" + "user": "user@localhost", + "password": "change me :)" }, "template": { "subject": "", diff --git a/config.go b/config.go index f808b22..8785375 100644 --- a/config.go +++ b/config.go @@ -11,7 +11,7 @@ type Config struct { EnabledApiEndpoints []string `json:"enabled_api_endpoints"` PersistentDataFilePath string `json:"datafile"` LogLevel int `json:"loglevel"` - Recipients []Recipient `json:"recipients"` + Lists *[]NotifyList `json:"lists"` SmtpConfiguration SmtpSettings `json:"smtp"` Template MailTemplateConfig `json:"template"` } @@ -23,12 +23,16 @@ func NewConfig() Config { EnabledApiEndpoints: []string{"bay", "bund"}, PersistentDataFilePath: "data.json", LogLevel: 2, - Recipients: []Recipient{}, + Lists: &[]NotifyList{ + { Name: "Example List", + Recipients: []string{}, + Filter: []Filter{},}, + }, SmtpConfiguration: SmtpSettings{ - From: "from@example.org", - User: "from@example.org", - Password: "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDüRfEnSiEnIcHt", - ServerHost: "example.org", + From: "user@localhost", + User: "user@localhost", + Password: "change me :)", + ServerHost: "127.0.0.1", ServerPort: 587}, Template: MailTemplateConfig{ SubjectTemplate: "", @@ -39,18 +43,20 @@ func NewConfig() Config { } func checkConfig(config Config) { - if len(config.Recipients) < 1 { + if len(*config.Lists) < 1 { logger.error("Configuration is incomplete") - panic(errors.New("no recipients are configured")) + panic(errors.New("no lists are configured")) } - for _, r := range config.Recipients { - if !mailAddressIsValid(r.Address) { - logger.error("Configuration includes invalid data") - panic(errors.New("'" + r.Address + "' is not a valid e-mail address")) - } - if len(r.Filters) < 1 { + for _, l := range *config.Lists { + if len(l.Filter) < 1 { logger.error("Configuration is incomplete") - panic(errors.New("recipient " + r.Address + " has no filter defined - at least {'any': true/false} should be configured")) + panic(errors.New("list " + l.Name + " has no filter defined - at least [{'any': true/false}] should be configured")) + } + for _, r := range l.Recipients { + if !mailAddressIsValid(r) { + logger.error("Configuration includes invalid data") + panic(errors.New("'" + r + "' is not a valid e-mail address")) + } } } if !mailAddressIsValid(config.SmtpConfiguration.From) { diff --git a/mail.go b/mail.go index 07477ba..d2f6ae4 100644 --- a/mail.go +++ b/mail.go @@ -23,21 +23,22 @@ func (c MailContent) serializeValidMail(from string, to string) []byte { // and I'm too lazy to encode ä into =E4 and so on subjectEncoded := base64.StdEncoding.EncodeToString([]byte(c.Subject)) bodyEncoded := base64.StdEncoding.EncodeToString([]byte(c.Body)) - data := []byte(fmt.Sprintf( + data := fmt.Appendf(nil, "Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: base64\r\nFrom: %v%vTo: %v%vSubject: =?utf-8?b?%v?=%v%v%v", from, MAIL_LINE_SEP, to, MAIL_LINE_SEP, subjectEncoded, MAIL_LINE_SEP, MAIL_LINE_SEP, - bodyEncoded)) + bodyEncoded) // done, I guess return data } -type Recipient struct { - Address string `json:"address"` +type NotifyList struct { + Name string `json:"name"` + Recipients []string `json:"recipients"` // Must be a configured filter id - Filters []Filter `json:"include"` + Filter []Filter `json:"filter"` } type SmtpSettings struct { @@ -48,8 +49,8 @@ type SmtpSettings struct { Password string `json:"password"` } -func (r Recipient) sendNotices(notices []WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { - logger.debug("Generating and sending mails to " + r.Address + " ...") +func sendNotices(recipient string, notices []WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { + logger.debug("Generating and sending mails for recipient " + recipient + " ...") cacheHits := 0 cacheMisses := 0 mails := [][]byte{} @@ -67,7 +68,7 @@ func (r Recipient) sendNotices(notices []WidNotice, template MailTemplate, auth logger.error(err) } // serialize mail - data = mailContent.serializeValidMail(smtpConfig.From, r.Address) + data = mailContent.serializeValidMail(smtpConfig.From, recipient) // add to cache (*cache)[n.Uuid] = data } @@ -77,11 +78,11 @@ func (r Recipient) sendNotices(notices []WidNotice, template MailTemplate, auth err := sendMails( smtpConfig, auth, - r.Address, + recipient, mails, ) if err != nil { return err } - logger.debug("Successfully sent all mails to " + r.Address) + logger.debug("Successfully sent all mails to " + recipient) return nil } diff --git a/main.go b/main.go index 62de84a..89456e3 100644 --- a/main.go +++ b/main.go @@ -120,22 +120,35 @@ func main() { logger.debug(fmt.Sprintf("Got %v new notices", len(newNotices))) if len(newNotices) > 0 { logger.info("Sending email notifications ...") + // mail recipient : pointer to slice of wid notices to be sent + noticesToBeSent := map[string][]WidNotice{} recipientsNotified := 0 var err error - for _, r := range config.Recipients { - // Filter notices for this recipient - filteredNotices := []WidNotice{} - for _, f := range r.Filters { + for _, l := range *config.Lists { + // Filter notices for this list + for _, f := range l.Filter { for _, n := range f.filter(newNotices) { - if !noticeSliceContains(filteredNotices, n) { - filteredNotices = append(filteredNotices, n) + for _, r := range l.Recipients { + if !noticeSliceContains(noticesToBeSent[r], n) { + noticesToBeSent[r] = append(noticesToBeSent[r], n) + } } } } - slices.Reverse(filteredNotices) - logger.debug(fmt.Sprintf("Including %v of %v notices for recipient %v", len(filteredNotices), len(newNotices), r.Address)) - // Send notices - err = r.sendNotices(filteredNotices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache) + } + for r, notices := range noticesToBeSent { + // sort by publish date + slices.SortFunc(notices, func(a WidNotice, b WidNotice) int { + if a.Published == b.Published { + return 0 + } else if a.Published.After(b.Published) { + return 1 + } else { + return -1 + } + }) + // send + err = sendNotices(r, notices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache) if err != nil { logger.error(err) } else { @@ -149,7 +162,7 @@ func main() { persistent.data.(PersistentData).LastPublished[id] = t persistent.save() } - logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(config.Recipients))) + logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(noticesToBeSent))) } } dt := int(time.Now().UnixMilli() - t1) From 9b00959fdb6d0c6a746b6cd83f4f041fba070acd Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 12 Jun 2025 17:23:08 +0200 Subject: [PATCH 5/8] Improve memory efficiency of noticesToBeSent by mapping pointers instead of WidNotice structs --- mail.go | 2 +- main.go | 9 +++++---- notice.go | 2 +- template.go | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mail.go b/mail.go index d2f6ae4..7cefa09 100644 --- a/mail.go +++ b/mail.go @@ -49,7 +49,7 @@ type SmtpSettings struct { Password string `json:"password"` } -func sendNotices(recipient string, notices []WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { +func sendNotices(recipient string, notices []*WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { logger.debug("Generating and sending mails for recipient " + recipient + " ...") cacheHits := 0 cacheMisses := 0 diff --git a/main.go b/main.go index 89456e3..8fc4ab0 100644 --- a/main.go +++ b/main.go @@ -121,16 +121,17 @@ func main() { if len(newNotices) > 0 { logger.info("Sending email notifications ...") // mail recipient : pointer to slice of wid notices to be sent - noticesToBeSent := map[string][]WidNotice{} + noticesToBeSent := map[string][]*WidNotice{} recipientsNotified := 0 var err error for _, l := range *config.Lists { // Filter notices for this list for _, f := range l.Filter { for _, n := range f.filter(newNotices) { + np := &n for _, r := range l.Recipients { - if !noticeSliceContains(noticesToBeSent[r], n) { - noticesToBeSent[r] = append(noticesToBeSent[r], n) + if !noticeSliceContains(noticesToBeSent[r], np) { + noticesToBeSent[r] = append(noticesToBeSent[r], np) } } } @@ -138,7 +139,7 @@ func main() { } for r, notices := range noticesToBeSent { // sort by publish date - slices.SortFunc(notices, func(a WidNotice, b WidNotice) int { + slices.SortFunc(notices, func(a *WidNotice, b *WidNotice) int { if a.Published == b.Published { return 0 } else if a.Published.After(b.Published) { diff --git a/notice.go b/notice.go index 758d85d..393f2c9 100644 --- a/notice.go +++ b/notice.go @@ -25,7 +25,7 @@ type WidNotice struct { PortalUrl string } -func noticeSliceContains(notices []WidNotice, notice WidNotice) bool { +func noticeSliceContains(notices []*WidNotice, notice *WidNotice) bool { for _, x := range notices { if x.Uuid == notice.Uuid { return true diff --git a/template.go b/template.go index 7d078e8..b07434c 100644 --- a/template.go +++ b/template.go @@ -33,7 +33,7 @@ Sent by WidNotifier {{ .WidNotifierVersion }} ` type TemplateData struct { - WidNotice + *WidNotice WidNotifierVersion string } From 30bf7935980bb87e386020acb402d252fde6c89f Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 12 Jun 2025 20:13:08 +0200 Subject: [PATCH 6/8] Encode mail subject as rfc2047 Q-Encoding and body as rfc2045 Quoted-Printable Encoding --- mail.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mail.go b/mail.go index 7cefa09..a37c933 100644 --- a/mail.go +++ b/mail.go @@ -4,33 +4,33 @@ package main import ( "crypto/tls" - "encoding/base64" "fmt" + "mime" + "mime/quotedprintable" "net/mail" "net/smtp" + "strings" "time" ) -const MAIL_LINE_SEP = "\r\n" - type MailContent struct { Subject string Body string } func (c MailContent) serializeValidMail(from string, to string) []byte { - // We'll send base64 encoded Subject & Body, because we Dschörmäns have umlauts - // and I'm too lazy to encode ä into =E4 and so on - subjectEncoded := base64.StdEncoding.EncodeToString([]byte(c.Subject)) - bodyEncoded := base64.StdEncoding.EncodeToString([]byte(c.Body)) + // format subject using Q Encoding from RFC2047 + subjectEncoded := mime.QEncoding.Encode("utf-8", c.Subject) + // format body using Quoted-Printable Encoding from RFC2045 + var bodyEncoded strings.Builder + bew := quotedprintable.NewWriter(&bodyEncoded) + bew.Write([]byte(c.Body)) + bew.Close() + // glue it all together data := fmt.Appendf(nil, - "Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: base64\r\nFrom: %v%vTo: %v%vSubject: =?utf-8?b?%v?=%v%v%v", - from, MAIL_LINE_SEP, - to, MAIL_LINE_SEP, - subjectEncoded, MAIL_LINE_SEP, - MAIL_LINE_SEP, - bodyEncoded) - // done, I guess + "Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: Quoted-Printable\r\nFrom: %v\r\nTo: %v\r\nSubject: %v\r\n\r\n%v", + from, to, subjectEncoded, bodyEncoded.String(), + ) return data } From a4fd43e03d81334a36ab701b2c21c8a73e4bd5f0 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 16 Jun 2025 17:00:58 +0200 Subject: [PATCH 7/8] Allow safe debugging of mail transfer by adding debug_mail_transfer build flag --- mail.go => mail_common.go | 44 ------------------------------- mail_transfer.go | 54 +++++++++++++++++++++++++++++++++++++++ mail_transfer_debug.go | 23 +++++++++++++++++ 3 files changed, 77 insertions(+), 44 deletions(-) rename mail.go => mail_common.go (63%) create mode 100644 mail_transfer.go create mode 100644 mail_transfer_debug.go diff --git a/mail.go b/mail_common.go similarity index 63% rename from mail.go rename to mail_common.go index a37c933..fee72f9 100644 --- a/mail.go +++ b/mail_common.go @@ -3,14 +3,12 @@ package main import ( - "crypto/tls" "fmt" "mime" "mime/quotedprintable" "net/mail" "net/smtp" "strings" - "time" ) type MailContent struct { @@ -86,48 +84,6 @@ func sendNotices(recipient string, notices []*WidNotice, template MailTemplate, return nil } -func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error { - addr := fmt.Sprintf("%v:%v", smtpConf.ServerHost, smtpConf.ServerPort) - logger.debug("Connecting to mail server at " + addr + " ...") - connection, err := smtp.Dial(addr) - if err != nil { return err } - defer connection.Close() - // can leave out connection.Hello - hasTlsExt, _ := connection.Extension("starttls") - if hasTlsExt { - err = connection.StartTLS(&tls.Config{ServerName: smtpConf.ServerHost}) - if err != nil { return err } - logger.debug("Mail Server supports TLS") - } else { - logger.debug("Mail Server doesn't support TLS") - } - logger.debug("Authenticating to mail server ...") - err = connection.Auth(auth) - if err != nil { return err } - if logger.LogLevel >= 3 { - fmt.Printf("DEBUG %v Sending mails to server ", time.Now().Format("2006/01/02 15:04:05.000000")) - } - for _, d := range data { - err = connection.Mail(smtpConf.From) - if err != nil { return err } - err = connection.Rcpt(to) - if err != nil { return err } - writer, err := connection.Data() - if err != nil { return err } - _, err = writer.Write(d) - if err != nil { return err } - err = writer.Close() - if err != nil { return err } - if logger.LogLevel >= 3 { - print(".") - } - } - if logger.LogLevel >= 3 { - print("\n") - } - return connection.Quit() -} - func mailAddressIsValid(address string) bool { _, err := mail.ParseAddress(address); return err == nil diff --git a/mail_transfer.go b/mail_transfer.go new file mode 100644 index 0000000..1013f0b --- /dev/null +++ b/mail_transfer.go @@ -0,0 +1,54 @@ +// +build !debug_mail_transfer +// Copyright (c) 2023 Julian Müller (ChaoticByte) + +package main + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "time" +) + + +func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error { + addr := fmt.Sprintf("%v:%v", smtpConf.ServerHost, smtpConf.ServerPort) + logger.debug("Connecting to mail server at " + addr + " ...") + connection, err := smtp.Dial(addr) + if err != nil { return err } + defer connection.Close() + // can leave out connection.Hello + hasTlsExt, _ := connection.Extension("starttls") + if hasTlsExt { + err = connection.StartTLS(&tls.Config{ServerName: smtpConf.ServerHost}) + if err != nil { return err } + logger.debug("Mail Server supports StartTLS") + } else { + logger.debug("Mail Server doesn't support StartTLS") + } + logger.debug("Authenticating to mail server ...") + err = connection.Auth(auth) + if err != nil { return err } + if logger.LogLevel >= 3 { + fmt.Printf("DEBUG %v Sending mails to server ", time.Now().Format("2006/01/02 15:04:05.000000")) + } + for _, d := range data { + err = connection.Mail(smtpConf.From) + if err != nil { return err } + err = connection.Rcpt(to) + if err != nil { return err } + writer, err := connection.Data() + if err != nil { return err } + _, err = writer.Write(d) + if err != nil { return err } + err = writer.Close() + if err != nil { return err } + if logger.LogLevel >= 3 { + print(".") + } + } + if logger.LogLevel >= 3 { + print("\n") + } + return connection.Quit() +} diff --git a/mail_transfer_debug.go b/mail_transfer_debug.go new file mode 100644 index 0000000..d3846a3 --- /dev/null +++ b/mail_transfer_debug.go @@ -0,0 +1,23 @@ +// +build debug_mail_transfer +// Copyright (c) 2023 Julian Müller (ChaoticByte) + +package main + +import ( + "fmt" + "net/smtp" +) + +func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error { + logger.warn("Mail Transfer Debugging is active. Not connecting.") + logger.info("MAIL TRANSFER: \n\n") + for _, d := range data { + fmt.Println("MAIL FROM:" + smtpConf.From) + fmt.Println("RCPT TO:" + to) + fmt.Println("DATA") + fmt.Println(string(d)) + fmt.Println(".") + } + fmt.Print("\n\n") + return nil +} From c1b3d106a0005c62ed40c6d9454cf6484a4975a8 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 16 Jun 2025 17:29:38 +0200 Subject: [PATCH 8/8] Fix leak of first recipient to other recipients because mail headers are also cached and re-used --- mail_common.go | 24 ++++++++++++------------ mail_transfer.go | 7 +++++-- mail_transfer_debug.go | 7 +++++-- main.go | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/mail_common.go b/mail_common.go index fee72f9..71e29bf 100644 --- a/mail_common.go +++ b/mail_common.go @@ -47,30 +47,30 @@ type SmtpSettings struct { Password string `json:"password"` } -func sendNotices(recipient string, notices []*WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { +func sendNotices(recipient string, notices []*WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, mailContentCache *map[string]*MailContent) error { logger.debug("Generating and sending mails for recipient " + recipient + " ...") cacheHits := 0 cacheMisses := 0 - mails := [][]byte{} + mails := []*MailContent{} for _, n := range notices { - var data []byte - cacheResult := (*cache)[n.Uuid] - if len(cacheResult) > 0 { + var mc *MailContent + cacheResult := (*mailContentCache)[n.Uuid] + if cacheResult != nil { cacheHits++ - data = cacheResult + mc = cacheResult } else { cacheMisses++ - mailContent, err := template.generate(TemplateData{n, Version}) + mc_, err := template.generate(TemplateData{n, Version}) if err != nil { logger.error("Could not create mail from template") logger.error(err) + } else { + mc = &mc_ + // add to cache + (*mailContentCache)[n.Uuid] = mc } - // serialize mail - data = mailContent.serializeValidMail(smtpConfig.From, recipient) - // add to cache - (*cache)[n.Uuid] = data } - mails = append(mails, data) + mails = append(mails, mc) } logger.debug(fmt.Sprintf("%v mail cache hits, %v misses", cacheHits, cacheMisses)) err := sendMails( diff --git a/mail_transfer.go b/mail_transfer.go index 1013f0b..45675d3 100644 --- a/mail_transfer.go +++ b/mail_transfer.go @@ -11,7 +11,7 @@ import ( ) -func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error { +func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, mails []*MailContent) error { addr := fmt.Sprintf("%v:%v", smtpConf.ServerHost, smtpConf.ServerPort) logger.debug("Connecting to mail server at " + addr + " ...") connection, err := smtp.Dial(addr) @@ -32,7 +32,10 @@ func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) if logger.LogLevel >= 3 { fmt.Printf("DEBUG %v Sending mails to server ", time.Now().Format("2006/01/02 15:04:05.000000")) } - for _, d := range data { + for _, mc := range mails { + // serialize mail + d := mc.serializeValidMail(smtpConf.From, to) + // send mail err = connection.Mail(smtpConf.From) if err != nil { return err } err = connection.Rcpt(to) diff --git a/mail_transfer_debug.go b/mail_transfer_debug.go index d3846a3..60fb2bd 100644 --- a/mail_transfer_debug.go +++ b/mail_transfer_debug.go @@ -8,10 +8,13 @@ import ( "net/smtp" ) -func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error { +func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, mails []*MailContent) error { logger.warn("Mail Transfer Debugging is active. Not connecting.") logger.info("MAIL TRANSFER: \n\n") - for _, d := range data { + for _, mc := range mails { + // serialize mail + d := mc.serializeValidMail(smtpConf.From, to) + // output mail fmt.Println("MAIL FROM:" + smtpConf.From) fmt.Println("RCPT TO:" + to) fmt.Println("DATA") diff --git a/main.go b/main.go index 8fc4ab0..d239bf0 100644 --- a/main.go +++ b/main.go @@ -98,7 +98,7 @@ func main() { t1 := time.Now().UnixMilli() newNotices := []WidNotice{} lastPublished := map[string]time.Time{} // endpoint id : last published timestamp - cache := map[string][]byte{} // cache generated emails for reuse + cache := map[string]*MailContent{} // cache generated emails for reuse for _, a := range enabledApiEndpoints { logger.info("Querying endpoint '" + a.Id + "' for new notices ...") n, t, err := a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id])