From 1af32ebf8f065f2ba722b333b6957b1c1123c7b9 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 25 Apr 2024 14:45:36 +1200 Subject: [PATCH 1/5] Chore: Improve tag sorting in web UI, ignore casing --- server/ui-src/components/Notifications.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/ui-src/components/Notifications.vue b/server/ui-src/components/Notifications.vue index 7f4871b5a..fa89c3dfa 100644 --- a/server/ui-src/components/Notifications.vue +++ b/server/ui-src/components/Notifications.vue @@ -64,9 +64,11 @@ export default { } for (let i in response.Data.Tags) { - if (mailbox.tags.indexOf(response.Data.Tags[i]) < 0) { + if (mailbox.tags.findIndex(e => { return e.toLowerCase() === response.Data.Tags[i].toLowerCase() }) < 0) { mailbox.tags.push(response.Data.Tags[i]) - mailbox.tags.sort() + mailbox.tags.sort((a, b) => { + return a.toLowerCase().localeCompare(b.toLowerCase()) + }) } } From 6585d450c02da0e0445670feb5a3c3106f03dbf5 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 25 Apr 2024 22:13:57 +1200 Subject: [PATCH 2/5] Feature: New search filter prefix `addressed:` includes From, To, Cc, Bcc & Reply-To --- internal/storage/search.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/storage/search.go b/internal/storage/search.go index b68ac6981..e3242c816 100644 --- a/internal/storage/search.go +++ b/internal/storage/search.go @@ -294,6 +294,16 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt { q.Where("ReplyToJSON LIKE ?", "%"+escPercentChar(w)+"%") } } + } else if strings.HasPrefix(lw, "addressed:") { + w = cleanString(w[10:]) + arg := "%" + escPercentChar(w) + "%" + if w != "" { + if exclude { + q.Where("(ToJSON NOT LIKE ? AND FromJSON NOT LIKE ? AND CcJSON NOT LIKE ? AND BccJSON NOT LIKE ? AND ReplyToJSON NOT LIKE ?)", arg, arg, arg, arg, arg) + } else { + q.Where("(ToJSON LIKE ? OR FromJSON LIKE ? OR CcJSON LIKE ? OR BccJSON LIKE ? OR ReplyToJSON LIKE ?)", arg, arg, arg, arg, arg) + } + } } else if strings.HasPrefix(lw, "subject:") { w = w[8:] if w != "" { From 15a5910695f3c31039961d81ab33c01323aadc33 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 25 Apr 2024 23:04:35 +1200 Subject: [PATCH 3/5] Feature: Search filter support for auto-tagging --- cmd/root.go | 2 +- internal/storage/database.go | 2 ++ internal/storage/messages.go | 24 +++++++------- internal/storage/tagfilters.go | 58 ++++++++++++++++++++++++++++++++++ internal/storage/tags.go | 52 ++++++++++++++++-------------- internal/tools/tags.go | 21 +++++++++--- 6 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 internal/storage/tagfilters.go diff --git a/cmd/root.go b/cmd/root.go index 631e6ff25..07645f501 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -128,7 +128,7 @@ func init() { // Tagging rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters") - rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "Convert new tags automatically to TitleCase") + rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "TitleCase new tags generated from plus-addresses and X-Tags") // Webhook rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages") diff --git a/internal/storage/database.go b/internal/storage/database.go index a74a2b589..7049af53b 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -108,6 +108,8 @@ func InitDB() error { return err } + LoadTagFilters() + dbFile = p dbLastAction = time.Now() diff --git a/internal/storage/messages.go b/internal/storage/messages.go index 1b6cb044a..500f44a80 100644 --- a/internal/storage/messages.go +++ b/internal/storage/messages.go @@ -71,13 +71,6 @@ func Store(body *[]byte) (string, error) { return "", err } - // extract tags from body matches based on --tag, plus addresses & X-Tags header - tagStr := findTagsInRawMessage(body) + "," + - obj.tagsFromPlusAddresses() + "," + - strings.TrimSpace(env.Root.Header.Get("X-Tags")) - - tagData := uniqueTagsFromString(tagStr) - // begin a transaction to ensure both the message // and data are stored successfully ctx := context.Background() @@ -119,9 +112,18 @@ func Store(body *[]byte) (string, error) { return "", err } - if len(tagData) > 0 { - // set tags after tx.Commit() - if err := SetMessageTags(id, tagData); err != nil { + // extract tags from body matches based on --tag, plus addresses & X-Tags header + rawTags := findTagsInRawMessage(body) + plusTags := obj.tagsFromPlusAddresses() + xTags := tools.SetTagCasing(strings.Split(strings.TrimSpace(env.Root.Header.Get("X-Tags")), ",")) + searchTags := TagFilterMatches(id) + + tags := append(rawTags, plusTags...) + tags = append(tags, xTags...) + tags = sortedUniqueTags(append(tags, searchTags...)) + + if len(tags) > 0 { + if err := SetMessageTags(id, tags); err != nil { return "", err } } @@ -137,7 +139,7 @@ func Store(body *[]byte) (string, error) { c.Attachments = attachments c.Subject = subject c.Size = size - c.Tags = tagData + c.Tags = tags c.Snippet = snippet websockets.Broadcast("new", c) diff --git a/internal/storage/tagfilters.go b/internal/storage/tagfilters.go new file mode 100644 index 000000000..a15d928f0 --- /dev/null +++ b/internal/storage/tagfilters.go @@ -0,0 +1,58 @@ +package storage + +import ( + "context" + "database/sql" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/logger" + "github.com/leporo/sqlf" +) + +// TagFilter struct +type TagFilter struct { + Search string + SQL *sqlf.Stmt + Tags []string +} + +var tagFilters = []TagFilter{} + +// LoadTagFilters loads tag filters from the config and pre-generates the SQL query +func LoadTagFilters() { + tagFilters = []TagFilter{} + + for _, t := range config.SMTPTags { + tagFilters = append(tagFilters, TagFilter{Search: t.Match, Tags: []string{t.Tag}, SQL: searchQueryBuilder(t.Match, "")}) + } +} + +// TagFilterMatches returns a slice of matching tags from a message +func TagFilterMatches(id string) []string { + tags := []string{} + + if len(tagFilters) == 0 { + return tags + } + + for _, f := range tagFilters { + var matchID string + q := f.SQL.Clone().Where("ID = ?", id) + if err := q.QueryAndClose(context.Background(), db, func(row *sql.Rows) { + var ignore sql.NullString + + if err := row.Scan(&ignore, &matchID, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil { + logger.Log().Errorf("[db] %s", err.Error()) + return + } + }); err != nil { + logger.Log().Errorf("[db] %s", err.Error()) + return tags + } + if matchID == id { + tags = append(tags, f.Tags...) + } + } + + return tags +} diff --git a/internal/storage/tags.go b/internal/storage/tags.go index 8b70cb6a5..8aeec6f08 100644 --- a/internal/storage/tags.go +++ b/internal/storage/tags.go @@ -1,6 +1,7 @@ package storage import ( + "bytes" "context" "database/sql" "regexp" @@ -19,7 +20,7 @@ var ( addTagMutex sync.RWMutex ) -// SetMessageTags will set the tags for a given database ID +// SetMessageTags will set the tags for a given database ID, removing any not in the array func SetMessageTags(id string, tags []string) error { applyTags := []string{} for _, t := range tags { @@ -33,7 +34,6 @@ func SetMessageTags(id string, tags []string) error { origTagCount := len(currentTags) for _, t := range applyTags { - t = tools.CleanTag(t) if t == "" || !config.ValidTagRegexp.MatchString(t) || inArray(t, currentTags) { continue } @@ -74,14 +74,15 @@ func AddMessageTag(id, name string) error { addTagMutex.Unlock() // check message does not already have this tag var count int - if _, err := sqlf.From(tenant("message_tags")). + + if err := sqlf.From(tenant("message_tags")). Select("COUNT(ID)").To(&count). Where("ID = ?", id). Where("TagID = ?", tagID). - ExecAndClose(context.TODO(), db); err != nil { + QueryRowAndClose(context.Background(), db); err != nil { return err } - if count != 0 { + if count > 0 { // already exists return nil } @@ -213,26 +214,28 @@ func pruneUnusedTags() error { return nil } -// Find tags set via --tags in raw message. +// Find tags set via --tags in raw message, useful for matching all headers etc. +// This function is largely superseded by the database searching, however this +// includes literally everything and is kept for backwards compatibility. // Returns a comma-separated string. -func findTagsInRawMessage(message *[]byte) string { - tagStr := "" +func findTagsInRawMessage(message *[]byte) []string { + tags := []string{} if len(config.SMTPTags) == 0 { - return tagStr + return tags } - str := strings.ToLower(string(*message)) + str := bytes.ToLower(*message) for _, t := range config.SMTPTags { - if strings.Contains(str, t.Match) { - tagStr += "," + t.Tag + if bytes.Contains(str, []byte(t.Match)) { + tags = append(tags, t.Tag) } } - return tagStr + return tags } // Returns tags found in email plus addresses (eg: test+tagname@example.com) -func (d DBMailSummary) tagsFromPlusAddresses() string { +func (d DBMailSummary) tagsFromPlusAddresses() []string { tags := []string{} for _, c := range d.To { matches := addressPlusRe.FindAllStringSubmatch(c.Address, 1) @@ -257,7 +260,7 @@ func (d DBMailSummary) tagsFromPlusAddresses() string { tags = append(tags, strings.Split(matches[0][2], "+")...) } - return strings.Join(tags, ",") + return tools.SetTagCasing(tags) } // Get message tags from the database for a given database ID @@ -282,24 +285,27 @@ func getMessageTags(id string) []string { return tags } -// UniqueTagsFromString will split a string with commas, and extract a unique slice of formatted tags -func uniqueTagsFromString(s string) []string { +// SortedUniqueTags will return a unique slice of normalised tags +func sortedUniqueTags(s []string) []string { tags := []string{} + added := make(map[string]bool) - if s == "" { + if len(s) == 0 { return tags } - parts := strings.Split(s, ",") - for _, p := range parts { + for _, p := range s { w := tools.CleanTag(p) if w == "" { continue } + lc := strings.ToLower(w) + if _, exists := added[lc]; exists { + continue + } if config.ValidTagRegexp.MatchString(w) { - if !inArray(w, tags) { - tags = append(tags, w) - } + added[lc] = true + tags = append(tags, w) } else { logger.Log().Debugf("[tags] ignoring invalid tag: %s", w) } diff --git a/internal/tools/tags.go b/internal/tools/tags.go index 8496e065f..a43f16dc1 100644 --- a/internal/tools/tags.go +++ b/internal/tools/tags.go @@ -19,18 +19,29 @@ var ( TagsTitleCase bool ) -// CleanTag returns a clean tag, removing whitespace and invalid characters +// CleanTag returns a clean tag, trimming whitespace and replacing invalid characters func CleanTag(s string) string { - s = strings.TrimSpace( + return strings.TrimSpace( multiSpaceRe.ReplaceAllString( tagsInvalidChars.ReplaceAllString(s, " "), " ", ), ) +} + +// SetTagCasing returns the slice of tags, title-casing if set +func SetTagCasing(s []string) []string { + if !TagsTitleCase { + return s + } + + titleTags := []string{} + + c := cases.Title(language.Und, cases.NoLower) - if TagsTitleCase { - return cases.Title(language.Und, cases.NoLower).String(s) + for _, t := range s { + titleTags = append(titleTags, c.String(t)) } - return s + return titleTags } From 65fb188586c90ce7905476d6bf4aee38826b5344 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 25 Apr 2024 23:18:46 +1200 Subject: [PATCH 4/5] Do not export autoTag struct --- config/config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 174f6475e..949e918ac 100644 --- a/config/config.go +++ b/config/config.go @@ -93,7 +93,7 @@ var ( ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.]){1,}$`) // SMTPTags are expressions to apply tags to new mail - SMTPTags []AutoTag + SMTPTags []autoTag // SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server SMTPRelayConfigFile string @@ -162,7 +162,7 @@ var ( ) // AutoTag struct for auto-tagging -type AutoTag struct { +type autoTag struct { Tag string Match string } @@ -381,7 +381,7 @@ func VerifyConfig() error { } } - SMTPTags = []AutoTag{} + SMTPTags = []autoTag{} if SMTPCLITags != "" { args := tools.ArgsParser(SMTPCLITags) @@ -397,7 +397,7 @@ func VerifyConfig() error { if len(match) == 0 { return fmt.Errorf("[tag] invalid tag match (%s) - no search detected", tag) } - SMTPTags = append(SMTPTags, AutoTag{Tag: tag, Match: match}) + SMTPTags = append(SMTPTags, autoTag{Tag: tag, Match: match}) } else { return fmt.Errorf("[tag] error parsing tags (%s)", a) } From dddc52a668c7ac115af5b7fc3eb96cc9da3e4bb3 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 26 Apr 2024 14:52:10 +1200 Subject: [PATCH 5/5] Feature: Set tagging filters via a config file --- cmd/root.go | 12 +++-- config/config.go | 42 +++++++----------- config/tags.go | 81 ++++++++++++++++++++++++++++++++++ internal/storage/messages.go | 9 +++- internal/storage/tagfilters.go | 38 +++++++++++++--- internal/storage/tags.go | 6 +-- internal/tools/utils.go | 11 +++++ 7 files changed, 154 insertions(+), 45 deletions(-) create mode 100644 config/tags.go create mode 100644 internal/tools/utils.go diff --git a/cmd/root.go b/cmd/root.go index 07645f501..e9344084e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -127,7 +127,8 @@ func init() { rootCmd.Flags().StringVar(&config.POP3TLSKey, "pop3-tls-key", config.POP3TLSKey, "Optional TLS key for POP3 server - requires pop3-tls-cert") // Tagging - rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters") + rootCmd.Flags().StringVarP(&config.CLITagsArg, "tag", "t", config.CLITagsArg, "Tag new messages matching filters") + rootCmd.Flags().StringVar(&config.TagsConfig, "tags-config", config.TagsConfig, "Load tags filters from yaml configuration file") rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "TitleCase new tags generated from plus-addresses and X-Tags") // Webhook @@ -283,12 +284,9 @@ func initConfigFromEnv() { config.POP3TLSKey = os.Getenv("MP_POP3_TLS_KEY") // Tagging - if len(os.Getenv("MP_TAG")) > 0 { - config.SMTPCLITags = os.Getenv("MP_TAG") - } - if getEnabledFromEnv("MP_TAGS_TITLE_CASE") { - tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE") - } + config.CLITagsArg = os.Getenv("MP_TAG") + config.TagsConfig = os.Getenv("MP_TAGS_CONFIG") + tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE") // Webhook if len(os.Getenv("MP_WEBHOOK_URL")) > 0 { diff --git a/config/config.go b/config/config.go index 949e918ac..b0d888cdb 100644 --- a/config/config.go +++ b/config/config.go @@ -15,7 +15,6 @@ import ( "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/spamassassin" - "github.com/axllent/mailpit/internal/tools" "gopkg.in/yaml.v3" ) @@ -86,14 +85,17 @@ var ( // BlockRemoteCSSAndFonts used to disable remote CSS & fonts BlockRemoteCSSAndFonts = false - // SMTPCLITags is used to map the CLI args - SMTPCLITags string + // CLITagsArg is used to map the CLI args + CLITagsArg string // ValidTagRegexp represents a valid tag ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.]){1,}$`) - // SMTPTags are expressions to apply tags to new mail - SMTPTags []autoTag + // TagsConfig is a yaml file to pre-load tags + TagsConfig string + + // TagFilters are used to apply tags to new mail + TagFilters []autoTag // SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server SMTPRelayConfigFile string @@ -163,8 +165,8 @@ var ( // AutoTag struct for auto-tagging type autoTag struct { - Tag string Match string + Tags []string } // SMTPRelayConfigStruct struct for parsing yaml & storing variables @@ -381,27 +383,13 @@ func VerifyConfig() error { } } - SMTPTags = []autoTag{} - - if SMTPCLITags != "" { - args := tools.ArgsParser(SMTPCLITags) - - for _, a := range args { - t := strings.Split(a, "=") - if len(t) > 1 { - tag := tools.CleanTag(t[0]) - if !ValidTagRegexp.MatchString(tag) || len(tag) == 0 { - return fmt.Errorf("[tag] invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag) - } - match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "="))) - if len(match) == 0 { - return fmt.Errorf("[tag] invalid tag match (%s) - no search detected", tag) - } - SMTPTags = append(SMTPTags, autoTag{Tag: tag, Match: match}) - } else { - return fmt.Errorf("[tag] error parsing tags (%s)", a) - } - } + // load tag filters + TagFilters = []autoTag{} + if err := loadTagsFromArgs(CLITagsArg); err != nil { + return err + } + if err := loadTagsFromConfig(TagsConfig); err != nil { + return err } if SMTPAllowedRecipients != "" { diff --git a/config/tags.go b/config/tags.go new file mode 100644 index 000000000..cb46f2599 --- /dev/null +++ b/config/tags.go @@ -0,0 +1,81 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/axllent/mailpit/internal/logger" + "github.com/axllent/mailpit/internal/tools" + "gopkg.in/yaml.v3" +) + +type yamlTags struct { + Filters []yamlTag `yaml:"filters"` +} + +type yamlTag struct { + Match string `yaml:"match"` + Tags string `yaml:"tags"` +} + +// Load tags from a configuration from a file, if set +func loadTagsFromConfig(c string) error { + if c == "" { + return nil // not set, ignore + } + + c = filepath.Clean(c) + + if !isFile(c) { + return fmt.Errorf("[tags] configuration file not found or unreadable: %s", c) + } + + data, err := os.ReadFile(c) + if err != nil { + return fmt.Errorf("[tags] %s", err.Error()) + } + + conf := yamlTags{} + + if err := yaml.Unmarshal(data, &conf); err != nil { + return err + } + + if conf.Filters == nil { + return fmt.Errorf("[tags] missing tag: array in %s", c) + } + + for _, t := range conf.Filters { + tags := strings.Split(t.Tags, ",") + TagFilters = append(TagFilters, autoTag{Match: t.Match, Tags: tags}) + } + + logger.Log().Debugf("[tags] loaded %s from config %s", tools.Plural(len(conf.Filters), "tag filter", "tag filters"), c) + + return nil +} + +func loadTagsFromArgs(c string) error { + if c == "" { + return nil // not set, ignore + } + + args := tools.ArgsParser(c) + + for _, a := range args { + t := strings.Split(a, "=") + if len(t) > 1 { + match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "="))) + tags := strings.Split(t[0], ",") + TagFilters = append(TagFilters, autoTag{Match: match, Tags: tags}) + } else { + return fmt.Errorf("[tag] error parsing tags (%s)", a) + } + } + + logger.Log().Debugf("[tags] loaded %s from CLI args", tools.Plural(len(args), "tag filter", "tag filters")) + + return nil +} diff --git a/internal/storage/messages.go b/internal/storage/messages.go index 500f44a80..638267232 100644 --- a/internal/storage/messages.go +++ b/internal/storage/messages.go @@ -112,14 +112,19 @@ func Store(body *[]byte) (string, error) { return "", err } - // extract tags from body matches based on --tag, plus addresses & X-Tags header + // extract tags from body matches rawTags := findTagsInRawMessage(body) + // extract plus addresses tags from enmime.Envelope plusTags := obj.tagsFromPlusAddresses() + // extract tags from X-Tags header xTags := tools.SetTagCasing(strings.Split(strings.TrimSpace(env.Root.Header.Get("X-Tags")), ",")) - searchTags := TagFilterMatches(id) + // extract tags from search matches + searchTags := tagFilterMatches(id) + // combine all tags into one slice tags := append(rawTags, plusTags...) tags = append(tags, xTags...) + // sort and extract only unique tags tags = sortedUniqueTags(append(tags, searchTags...)) if len(tags) > 0 { diff --git a/internal/storage/tagfilters.go b/internal/storage/tagfilters.go index a15d928f0..2990235ef 100644 --- a/internal/storage/tagfilters.go +++ b/internal/storage/tagfilters.go @@ -3,17 +3,19 @@ package storage import ( "context" "database/sql" + "strings" "github.com/axllent/mailpit/config" "github.com/axllent/mailpit/internal/logger" + "github.com/axllent/mailpit/internal/tools" "github.com/leporo/sqlf" ) // TagFilter struct type TagFilter struct { - Search string - SQL *sqlf.Stmt - Tags []string + Match string + SQL *sqlf.Stmt + Tags []string } var tagFilters = []TagFilter{} @@ -22,13 +24,37 @@ var tagFilters = []TagFilter{} func LoadTagFilters() { tagFilters = []TagFilter{} - for _, t := range config.SMTPTags { - tagFilters = append(tagFilters, TagFilter{Search: t.Match, Tags: []string{t.Tag}, SQL: searchQueryBuilder(t.Match, "")}) + for _, t := range config.TagFilters { + match := strings.TrimSpace(t.Match) + if match == "" { + logger.Log().Warnf("[tags] ignoring tag item with missing 'match'") + continue + } + if t.Tags == nil || len(t.Tags) == 0 { + logger.Log().Warnf("[tags] ignoring tag items with missing 'tags' array") + continue + } + + validTags := []string{} + for _, tag := range t.Tags { + tagName := tools.CleanTag(tag) + if !config.ValidTagRegexp.MatchString(tagName) || len(tagName) == 0 { + logger.Log().Warnf("[tags] invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tagName) + continue + } + validTags = append(validTags, tagName) + } + + if len(validTags) == 0 { + continue + } + + tagFilters = append(tagFilters, TagFilter{Match: match, Tags: validTags, SQL: searchQueryBuilder(match, "")}) } } // TagFilterMatches returns a slice of matching tags from a message -func TagFilterMatches(id string) []string { +func tagFilterMatches(id string) []string { tags := []string{} if len(tagFilters) == 0 { diff --git a/internal/storage/tags.go b/internal/storage/tags.go index 8aeec6f08..c28130029 100644 --- a/internal/storage/tags.go +++ b/internal/storage/tags.go @@ -220,14 +220,14 @@ func pruneUnusedTags() error { // Returns a comma-separated string. func findTagsInRawMessage(message *[]byte) []string { tags := []string{} - if len(config.SMTPTags) == 0 { + if len(tagFilters) == 0 { return tags } str := bytes.ToLower(*message) - for _, t := range config.SMTPTags { + for _, t := range tagFilters { if bytes.Contains(str, []byte(t.Match)) { - tags = append(tags, t.Tag) + tags = append(tags, t.Tags...) } } diff --git a/internal/tools/utils.go b/internal/tools/utils.go new file mode 100644 index 000000000..f152c6e14 --- /dev/null +++ b/internal/tools/utils.go @@ -0,0 +1,11 @@ +package tools + +import "fmt" + +// Plural returns a singular or plural of a word together with the total +func Plural(total int, singular, plural string) string { + if total == 1 { + return fmt.Sprintf("%d %s", total, singular) + } + return fmt.Sprintf("%d %s", total, plural) +}