diff --git a/.gitignore b/.gitignore index 4ef31b5..b64f1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,3 @@ dist/ data config *.bak - -# Config/data files -*.json diff --git a/README.md b/README.md index e04f337..e584424 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WidNotifier +# WID Notifier The German [BSI](https://www.bsi.bund.de/) and [LSI Bavaria](https://lsi.bayern.de/) each have a page listing current security notices. This software queries the APIs of these services for new security notices and sends configurable email notifications. @@ -17,7 +17,7 @@ This Software only supports Linux. # Build To cross-compile the software for `i386`, `amd64`, `arm` and `arm64`, run `build.sh`. -You need Go 1.22.x and git. +You need a go version >= 1.21 and git. # Usage @@ -34,28 +34,27 @@ Example: ```json { "api_fetch_interval": 600, + "datafile": "data", "enabled_api_endpoints": [ "bay", "bund" ], - "datafile": "data.json", "loglevel": 2, - "lists": [ + "recipients": [ { - "name": "Example List", - "recipients": ["someone@example.org"], - "filter": [ - {"classification": "hoch", "title_contains": "Microsoft"}, - {"classification": "kritisch"} + "address": "guenther@example.org", + "include": [ + {"classification": "kritisch"}, + {"title_contains": "jQuery"} ] } ], "smtp": { - "from": "user@localhost", - "host": "127.0.0.1", + "from": "from@example.org", + "host": "example.org", "port": 587, - "user": "user@localhost", - "password": "change me :)" + "user": "from@example.org", + "password": "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDüRfEnSiEnIcHt" }, "template": { "subject": "", @@ -68,9 +67,9 @@ To show debug messages, set the `loglevel` to `3`. ## Filters -You define filters for notices to be sent (per recipient). Multiple filters can be set per recipient and multiple criteria can be used per filter. The configuration field for those filters is `include`. See [Configuration](#configuration) for an example. +You define filters for notices to be sent per recipient. Multiple filters can be set per recipient and multiple criteria can be used per filter. The configuration field for those filters is `include`. See [Configuration](#configuration) for an example. -If a notice is included is determined by the following logic: +It is determined by the following logic, if a notice is included: ``` {criteria, criteria, ... ALL APPLY} @@ -78,7 +77,7 @@ OR {criteria, criteria, ... ALL APPLY} OR ... ``` -The following criteria are available. Criteria marked with * are optional fields that are not supported by every API endpoint (e.g. https://wid.lsi.bayern.de) - notices from those endpoints will therefore not be included when using those criteria in filters. +The following criteria are available. Criteria marked with `*` are for optional fields that are not supported by every API endpoint (e.g. https://wid.lsi.bayern.de) - notices from those endpoints will therefore not be included when using those criteria in filters. ```json "include": [ @@ -123,7 +122,7 @@ Classification can be `"kritisch"`, `"hoch"`, `"mittel"` or `"niedrig"`. ``` If set to `""`, this criteria will be ignored. -### min_basescore * +### min_basescore `*` Include notices whose basescore (`0` - `100`) is >= `min_basescore`. @@ -132,7 +131,7 @@ Include notices whose basescore (`0` - `100`) is >= `min_basescore`. ``` This criteria will be ignored if set to `0`. -### status * +### status `*` Include notices with this status. This is usually either `NEU` or `UPDATE`. @@ -141,7 +140,7 @@ Include notices with this status. This is usually either `NEU` or `UPDATE`. ``` If set to `""`, this criteria will be ignored. -### products_contain * +### products_contain `*` Include notices whose product list contains this text. @@ -150,7 +149,7 @@ Include notices whose product list contains this text. ``` If set to `""`, this criteria will be ignored. -### no_patch * +### no_patch `*` If set to `"true"`, notices where no patch is available will be included. @@ -203,6 +202,4 @@ type WidNotice struct { } ``` -Additionally, the field `WidNotifierVersion` holds the version of the software. - For an example, take a look at `DEFAULT_SUBJECT_TEMPLATE` and `DEFAULT_BODY_TEMPLATE` in [template.go](./template.go). diff --git a/build.sh b/build.sh index 8418388..7ac90ca 100755 --- a/build.sh +++ b/build.sh @@ -2,13 +2,14 @@ VERSION=$(git describe --tags) -function build() { - printf "Compiling version ${VERSION} for ${1}/${2}\t" - GOOS=${1} GOARCH=${2} go build -ldflags "-X 'main.Version=${VERSION}'" -o dist/wid-notifier_${VERSION}_${1}_${3} - echo "✅" -} +# i386 +GOOS=linux GOARCH=386 go build -o dist/wid-notifier_${VERSION}_linux_i386 -build linux "386" i386 -build linux amd64 amd64 -build linux arm arm -build linux arm64 arm64 +# amd64 +GOOS=linux GOARCH=amd64 go build -o dist/wid-notifier_${VERSION}_linux_amd64 + +# arm +GOOS=linux GOARCH=arm go build -o dist/wid-notifier_${VERSION}_linux_arm + +# arm64 +GOOS=linux GOARCH=arm64 go build -o dist/wid-notifier_${VERSION}_linux_arm64 diff --git a/config.go b/config.go deleted file mode 100644 index 8785375..0000000 --- a/config.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2024 Julian Müller (ChaoticByte) - -package main - -import ( - "errors" -) - -type Config struct { - ApiFetchInterval int `json:"api_fetch_interval"` // in seconds - EnabledApiEndpoints []string `json:"enabled_api_endpoints"` - PersistentDataFilePath string `json:"datafile"` - LogLevel int `json:"loglevel"` - Lists *[]NotifyList `json:"lists"` - SmtpConfiguration SmtpSettings `json:"smtp"` - Template MailTemplateConfig `json:"template"` -} - -func NewConfig() Config { - // Initial config - c := Config{ - ApiFetchInterval: 60 * 10, // every 10 minutes, - EnabledApiEndpoints: []string{"bay", "bund"}, - PersistentDataFilePath: "data.json", - LogLevel: 2, - Lists: &[]NotifyList{ - { Name: "Example List", - Recipients: []string{}, - Filter: []Filter{},}, - }, - SmtpConfiguration: SmtpSettings{ - From: "user@localhost", - User: "user@localhost", - Password: "change me :)", - ServerHost: "127.0.0.1", - ServerPort: 587}, - Template: MailTemplateConfig{ - SubjectTemplate: "", - BodyTemplate: "", - }, - } - return c -} - -func checkConfig(config Config) { - if len(*config.Lists) < 1 { - logger.error("Configuration is incomplete") - panic(errors.New("no lists are configured")) - } - for _, l := range *config.Lists { - if len(l.Filter) < 1 { - logger.error("Configuration is incomplete") - 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) { - logger.error("Configuration includes invalid data") - panic(errors.New("'" + config.SmtpConfiguration.From + "' is not a valid e-mail address")) - } -} diff --git a/datastore.go b/datastore.go index 128aca9..fbc09e4 100644 --- a/datastore.go +++ b/datastore.go @@ -4,10 +4,79 @@ package main import ( "encoding/json" + "errors" "io/fs" "os" + "time" ) +type Config struct { + ApiFetchInterval int `json:"api_fetch_interval"` // in seconds + EnabledApiEndpoints []string `json:"enabled_api_endpoints"` + PersistentDataFilePath string `json:"datafile"` + LogLevel int `json:"loglevel"` + Recipients []Recipient `json:"recipients"` + SmtpConfiguration SmtpSettings `json:"smtp"` + Template MailTemplateConfig `json:"template"` +} + +func NewConfig() Config { + // Initial config + c := Config{ + ApiFetchInterval: 60 * 10, // every 10 minutes, + EnabledApiEndpoints: []string{"bay", "bund"}, + PersistentDataFilePath: "data", + LogLevel: 2, + Recipients: []Recipient{}, + SmtpConfiguration: SmtpSettings{ + From: "from@example.org", + User: "from@example.org", + Password: "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDüRfEnSiEnIcHt", + ServerHost: "example.org", + ServerPort: 587}, + Template: MailTemplateConfig{ + SubjectTemplate: "", + BodyTemplate: "", + }, + } + return c +} + +func checkConfig(config Config) { + if len(config.Recipients) < 1 { + logger.error("Configuration is incomplete") + panic(errors.New("no recipients 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 { + logger.error("Configuration is incomplete") + panic(errors.New("recipient " + r.Address + " has no filter defined - at least {'any': true/false} should be configured")) + } + } + if !mailAddressIsValid(config.SmtpConfiguration.From) { + logger.error("Configuration includes invalid data") + panic(errors.New("'" + config.SmtpConfiguration.From + "' is not a valid e-mail address")) + } +} + +type PersistentData struct { + // {endpoint id 1: time last published, endpoint id 2: ..., ...} + LastPublished map[string]time.Time `json:"last_published"` +} + +func NewPersistentData(c Config) PersistentData { + // Initial persistent data + d := PersistentData{LastPublished: map[string]time.Time{}} + for _, e := range apiEndpoints { + d.LastPublished[e.Id] = time.Now().Add(-time.Hour * 24) // a day ago + } + return d +} + type DataStore struct { filepath string prettyJSON bool @@ -23,24 +92,32 @@ func (ds *DataStore) save() error { } else { data, err = json.Marshal(ds.data) } - if err != nil { return err } + if err != nil { + return err + } err = os.WriteFile(ds.filepath, data, ds.fileMode) return err } func (ds *DataStore) load() error { data, err := os.ReadFile(ds.filepath) - if err != nil { return err } + if err != nil { + return err + } switch ds.data.(type) { case Config: d, _ := ds.data.(Config); err = json.Unmarshal(data, &d) - if err != nil { return err } + if err != nil { + return err + } ds.data = d case PersistentData: d, _ := ds.data.(PersistentData); err = json.Unmarshal(data, &d) - if err != nil { return err } + if err != nil { + return err + } ds.data = d } return err diff --git a/go.mod b/go.mod index acbf5d6..d8205ab 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/ChaoticByte/wid-notifier -go 1.24 +go 1.22.2 diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..af43e38 --- /dev/null +++ b/mail.go @@ -0,0 +1,162 @@ +// Copyright (c) 2023 Julian Müller (ChaoticByte) + +package main + +import ( + "crypto/tls" + "encoding/base64" + "fmt" + "net/mail" + "net/smtp" + "slices" + "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)) + data := []byte(fmt.Sprintf( + "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 + return data +} + +type Recipient struct { + Address string `json:"address"` + // Must be a configured filter id + Filters []Filter `json:"include"` +} + +type SmtpSettings struct { + From string `json:"from"` + ServerHost string `json:"host"` + ServerPort int `json:"port"` + User string `json:"user"` + Password string `json:"password"` +} + +func (r Recipient) filterAndSendNotices(notices []WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error { + filteredNotices := []WidNotice{} + for _, f := range r.Filters { + for _, n := range f.filter(notices) { + if !noticeSliceContains(filteredNotices, n) { + filteredNotices = append(filteredNotices, n) + } + } + } + slices.Reverse(filteredNotices) + logger.debug(fmt.Sprintf("Including %v of %v notices for recipient %v", len(filteredNotices), len(notices), r.Address)) + logger.debug("Generating and sending mails to " + r.Address + " ...") + cacheHits := 0 + cacheMisses := 0 + mails := [][]byte{} + for _, n := range filteredNotices { + var data []byte + cacheResult := (*cache)[n.Uuid] + if len(cacheResult) > 0 { + cacheHits++ + data = cacheResult + } else { + cacheMisses++ + mailContent, err := template.generate(n) + if err != nil { + logger.error("Could not create mail from template") + logger.error(err) + } + // serialize & send mail + data = mailContent.serializeValidMail(smtpConfig.From, r.Address) + // add to cache + (*cache)[n.Uuid] = data + } + mails = append(mails, data) + } + logger.debug(fmt.Sprintf("%v mail cache hits, %v misses", cacheHits, cacheMisses)) + err := sendMails( + smtpConfig, + auth, + r.Address, + mails, + ) + if err != nil { + return err + } + logger.debug("Successfully sent all mails to " + r.Address) + 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_common.go b/mail_common.go deleted file mode 100644 index 71e29bf..0000000 --- a/mail_common.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2023 Julian Müller (ChaoticByte) - -package main - -import ( - "fmt" - "mime" - "mime/quotedprintable" - "net/mail" - "net/smtp" - "strings" -) - -type MailContent struct { - Subject string - Body string -} - -func (c MailContent) serializeValidMail(from string, to string) []byte { - // 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: Quoted-Printable\r\nFrom: %v\r\nTo: %v\r\nSubject: %v\r\n\r\n%v", - from, to, subjectEncoded, bodyEncoded.String(), - ) - return data -} - -type NotifyList struct { - Name string `json:"name"` - Recipients []string `json:"recipients"` - // Must be a configured filter id - Filter []Filter `json:"filter"` -} - -type SmtpSettings struct { - From string `json:"from"` - ServerHost string `json:"host"` - ServerPort int `json:"port"` - User string `json:"user"` - Password string `json:"password"` -} - -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 := []*MailContent{} - for _, n := range notices { - var mc *MailContent - cacheResult := (*mailContentCache)[n.Uuid] - if cacheResult != nil { - cacheHits++ - mc = cacheResult - } else { - cacheMisses++ - 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 - } - } - mails = append(mails, mc) - } - logger.debug(fmt.Sprintf("%v mail cache hits, %v misses", cacheHits, cacheMisses)) - err := sendMails( - smtpConfig, - auth, - recipient, - mails, - ) - if err != nil { return err } - logger.debug("Successfully sent all mails to " + recipient) - return nil -} - -func mailAddressIsValid(address string) bool { - _, err := mail.ParseAddress(address); - return err == nil -} diff --git a/mail_transfer.go b/mail_transfer.go deleted file mode 100644 index 45675d3..0000000 --- a/mail_transfer.go +++ /dev/null @@ -1,57 +0,0 @@ -// +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, 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) - 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 _, 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) - 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 deleted file mode 100644 index 60fb2bd..0000000 --- a/mail_transfer_debug.go +++ /dev/null @@ -1,26 +0,0 @@ -// +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, mails []*MailContent) error { - logger.warn("Mail Transfer Debugging is active. Not connecting.") - logger.info("MAIL TRANSFER: \n\n") - 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") - fmt.Println(string(d)) - fmt.Println(".") - } - fmt.Print("\n\n") - return nil -} diff --git a/main.go b/main.go index d239bf0..c39f6bc 100644 --- a/main.go +++ b/main.go @@ -6,55 +6,35 @@ import ( "fmt" "net/smtp" "os" - "slices" "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 { - showHelp() + fmt.Printf("Usage: %v \nIf the config file doesn't exist, a incomplete configuration with default values is created.\n", args[0]) os.Exit(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] + configFilePath := os.Args[1] // create logger logger = NewLogger(2) // init logger.info("Initializing ...") defer logger.info("Exiting ...") - // open & check config config := NewDataStore( configFilePath, NewConfig(), true, 0600, ).data.(Config) + persistent := NewDataStore( + config.PersistentDataFilePath, + NewPersistentData(config), + false, + 0640) logger.LogLevel = config.LogLevel logger.debug("Checking configuration file ...") checkConfig(config) @@ -86,24 +66,17 @@ func main() { } } } - // open data file - persistent := NewDataStore( - config.PersistentDataFilePath, - NewPersistentData(config), - false, - 0640) // main loop logger.debug("Entering main loop ...") for { t1 := time.Now().UnixMilli() newNotices := []WidNotice{} - lastPublished := map[string]time.Time{} // endpoint id : last published timestamp - cache := map[string]*MailContent{} // cache generated emails for reuse + cache := map[string][]byte{} for _, a := range enabledApiEndpoints { logger.info("Querying endpoint '" + a.Id + "' for new notices ...") n, t, err := a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id]) if err != nil { - // retry (once) + // retry logger.warn("Couldn't query notices from API endpoint '" + a.Id + "'. Retrying ...") logger.warn(err) n, t, err = a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id]) @@ -114,59 +87,26 @@ func main() { logger.error(err) } else if len(n) > 0 { newNotices = append(newNotices, n...) - lastPublished[a.Id] = t + persistent.data.(PersistentData).LastPublished[a.Id] = t + persistent.save() } } 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 _, 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], np) { - noticesToBeSent[r] = append(noticesToBeSent[r], np) - } - } - } - } - } - 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) + for _, r := range config.Recipients { + err := r.filterAndSendNotices(newNotices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache) if err != nil { logger.error(err) } else { recipientsNotified++ } } - 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(noticesToBeSent))) - } + logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(config.Recipients))) } - dt := int(time.Now().UnixMilli() - t1) + t2 := time.Now().UnixMilli() + dt := int(t2 - t1) time.Sleep(time.Millisecond * time.Duration((config.ApiFetchInterval * 1000) - dt)) } } diff --git a/notice.go b/notice.go index 393f2c9..8967d20 100644 --- a/notice.go +++ b/notice.go @@ -25,7 +25,17 @@ type WidNotice struct { PortalUrl string } -func noticeSliceContains(notices []*WidNotice, notice *WidNotice) bool { +// 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 { return true diff --git a/persistent_data.go b/persistent_data.go deleted file mode 100644 index f3ce9ce..0000000 --- a/persistent_data.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2024 Julian Müller (ChaoticByte) - -package main - -import ( - "time" -) - -type PersistentData struct { - // {endpoint id 1: time last published, endpoint id 2: ..., ...} - LastPublished map[string]time.Time `json:"last_published"` -} - -func NewPersistentData(c Config) PersistentData { - // Initial persistent data - d := PersistentData{LastPublished: map[string]time.Time{}} - for _, e := range apiEndpoints { - d.LastPublished[e.Id] = time.Now().Add(-time.Hour * 24) // a day ago - } - return d -} diff --git a/template.go b/template.go index b07434c..beda4ac 100644 --- a/template.go +++ b/template.go @@ -26,16 +26,7 @@ Affected Products:{{ range $product := .ProductNames }} Assigned CVEs:{{ range $cve := .Cves }} - {{ $cve }} -> https://www.cve.org/CVERecord?id={{ $cve }} -{{- end }}{{ end }} - - -Sent by WidNotifier {{ .WidNotifierVersion }} -` - -type TemplateData struct { - *WidNotice - WidNotifierVersion string -} +{{- end }}{{ end }}` type MailTemplateConfig struct { SubjectTemplate string `json:"subject"` @@ -47,15 +38,19 @@ type MailTemplate struct { BodyTemplate template.Template } -func (t MailTemplate) generate(data TemplateData) (MailContent, error) { +func (t MailTemplate) generate(notice WidNotice) (MailContent, error) { c := MailContent{} buffer := &bytes.Buffer{} - err := t.SubjectTemplate.Execute(buffer, data) - if err != nil { return c, err } + err := t.SubjectTemplate.Execute(buffer, notice) + if err != nil { + return c, err + } c.Subject = buffer.String() buffer.Truncate(0) // we can recycle our buffer - err = t.BodyTemplate.Execute(buffer, data) - if err != nil { return c, err } + err = t.BodyTemplate.Execute(buffer, notice) + if err != nil { + return c, err + } c.Body = buffer.String() return c, nil } diff --git a/version.go b/version.go deleted file mode 100644 index f0ce3c0..0000000 --- a/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -var Version = "devel" diff --git a/widapi.go b/widapi.go index 0ef572e..28fe259 100644 --- a/widapi.go +++ b/widapi.go @@ -57,7 +57,9 @@ func (e ApiEndpoint) getNotices(since time.Time) ([]WidNotice, time.Time, error) if err == nil { if res.StatusCode == 200 { resBody, err := io.ReadAll(res.Body) - if err != nil { return []WidNotice{}, since, err } + if err != nil { + return []WidNotice{}, since, err + } var decodedData map[string]interface{} if err = json.Unmarshal(resBody, &decodedData); err != nil { return []WidNotice{}, since, err