mirror of
https://github.com/axllent/mailpit.git
synced 2025-04-04 22:34:31 +02:00
Feature: Set tagging filters via a config file
This commit is contained in:
parent
65fb188586
commit
dddc52a668
12
cmd/root.go
12
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")
|
rootCmd.Flags().StringVar(&config.POP3TLSKey, "pop3-tls-key", config.POP3TLSKey, "Optional TLS key for POP3 server - requires pop3-tls-cert")
|
||||||
|
|
||||||
// Tagging
|
// 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")
|
rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "TitleCase new tags generated from plus-addresses and X-Tags")
|
||||||
|
|
||||||
// Webhook
|
// Webhook
|
||||||
@ -283,12 +284,9 @@ func initConfigFromEnv() {
|
|||||||
config.POP3TLSKey = os.Getenv("MP_POP3_TLS_KEY")
|
config.POP3TLSKey = os.Getenv("MP_POP3_TLS_KEY")
|
||||||
|
|
||||||
// Tagging
|
// Tagging
|
||||||
if len(os.Getenv("MP_TAG")) > 0 {
|
config.CLITagsArg = os.Getenv("MP_TAG")
|
||||||
config.SMTPCLITags = os.Getenv("MP_TAG")
|
config.TagsConfig = os.Getenv("MP_TAGS_CONFIG")
|
||||||
}
|
tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE")
|
||||||
if getEnabledFromEnv("MP_TAGS_TITLE_CASE") {
|
|
||||||
tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webhook
|
// Webhook
|
||||||
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
|
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
|
||||||
|
@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/axllent/mailpit/internal/auth"
|
"github.com/axllent/mailpit/internal/auth"
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/spamassassin"
|
"github.com/axllent/mailpit/internal/spamassassin"
|
||||||
"github.com/axllent/mailpit/internal/tools"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,14 +85,17 @@ var (
|
|||||||
// BlockRemoteCSSAndFonts used to disable remote CSS & fonts
|
// BlockRemoteCSSAndFonts used to disable remote CSS & fonts
|
||||||
BlockRemoteCSSAndFonts = false
|
BlockRemoteCSSAndFonts = false
|
||||||
|
|
||||||
// SMTPCLITags is used to map the CLI args
|
// CLITagsArg is used to map the CLI args
|
||||||
SMTPCLITags string
|
CLITagsArg string
|
||||||
|
|
||||||
// ValidTagRegexp represents a valid tag
|
// ValidTagRegexp represents a valid tag
|
||||||
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.]){1,}$`)
|
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.]){1,}$`)
|
||||||
|
|
||||||
// SMTPTags are expressions to apply tags to new mail
|
// TagsConfig is a yaml file to pre-load tags
|
||||||
SMTPTags []autoTag
|
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 to parse a yaml file and store config of relay SMTP server
|
||||||
SMTPRelayConfigFile string
|
SMTPRelayConfigFile string
|
||||||
@ -163,8 +165,8 @@ var (
|
|||||||
|
|
||||||
// AutoTag struct for auto-tagging
|
// AutoTag struct for auto-tagging
|
||||||
type autoTag struct {
|
type autoTag struct {
|
||||||
Tag string
|
|
||||||
Match string
|
Match string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
|
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
|
||||||
@ -381,27 +383,13 @@ func VerifyConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SMTPTags = []autoTag{}
|
// load tag filters
|
||||||
|
TagFilters = []autoTag{}
|
||||||
if SMTPCLITags != "" {
|
if err := loadTagsFromArgs(CLITagsArg); err != nil {
|
||||||
args := tools.ArgsParser(SMTPCLITags)
|
return err
|
||||||
|
}
|
||||||
for _, a := range args {
|
if err := loadTagsFromConfig(TagsConfig); err != nil {
|
||||||
t := strings.Split(a, "=")
|
return err
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if SMTPAllowedRecipients != "" {
|
if SMTPAllowedRecipients != "" {
|
||||||
|
81
config/tags.go
Normal file
81
config/tags.go
Normal file
@ -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
|
||||||
|
}
|
@ -112,14 +112,19 @@ func Store(body *[]byte) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract tags from body matches based on --tag, plus addresses & X-Tags header
|
// extract tags from body matches
|
||||||
rawTags := findTagsInRawMessage(body)
|
rawTags := findTagsInRawMessage(body)
|
||||||
|
// extract plus addresses tags from enmime.Envelope
|
||||||
plusTags := obj.tagsFromPlusAddresses()
|
plusTags := obj.tagsFromPlusAddresses()
|
||||||
|
// extract tags from X-Tags header
|
||||||
xTags := tools.SetTagCasing(strings.Split(strings.TrimSpace(env.Root.Header.Get("X-Tags")), ","))
|
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(rawTags, plusTags...)
|
||||||
tags = append(tags, xTags...)
|
tags = append(tags, xTags...)
|
||||||
|
// sort and extract only unique tags
|
||||||
tags = sortedUniqueTags(append(tags, searchTags...))
|
tags = sortedUniqueTags(append(tags, searchTags...))
|
||||||
|
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
|
@ -3,17 +3,19 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
"github.com/leporo/sqlf"
|
"github.com/leporo/sqlf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagFilter struct
|
// TagFilter struct
|
||||||
type TagFilter struct {
|
type TagFilter struct {
|
||||||
Search string
|
Match string
|
||||||
SQL *sqlf.Stmt
|
SQL *sqlf.Stmt
|
||||||
Tags []string
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var tagFilters = []TagFilter{}
|
var tagFilters = []TagFilter{}
|
||||||
@ -22,13 +24,37 @@ var tagFilters = []TagFilter{}
|
|||||||
func LoadTagFilters() {
|
func LoadTagFilters() {
|
||||||
tagFilters = []TagFilter{}
|
tagFilters = []TagFilter{}
|
||||||
|
|
||||||
for _, t := range config.SMTPTags {
|
for _, t := range config.TagFilters {
|
||||||
tagFilters = append(tagFilters, TagFilter{Search: t.Match, Tags: []string{t.Tag}, SQL: searchQueryBuilder(t.Match, "")})
|
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
|
// TagFilterMatches returns a slice of matching tags from a message
|
||||||
func TagFilterMatches(id string) []string {
|
func tagFilterMatches(id string) []string {
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
|
|
||||||
if len(tagFilters) == 0 {
|
if len(tagFilters) == 0 {
|
||||||
|
@ -220,14 +220,14 @@ func pruneUnusedTags() error {
|
|||||||
// Returns a comma-separated string.
|
// Returns a comma-separated string.
|
||||||
func findTagsInRawMessage(message *[]byte) []string {
|
func findTagsInRawMessage(message *[]byte) []string {
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
if len(config.SMTPTags) == 0 {
|
if len(tagFilters) == 0 {
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
str := bytes.ToLower(*message)
|
str := bytes.ToLower(*message)
|
||||||
for _, t := range config.SMTPTags {
|
for _, t := range tagFilters {
|
||||||
if bytes.Contains(str, []byte(t.Match)) {
|
if bytes.Contains(str, []byte(t.Match)) {
|
||||||
tags = append(tags, t.Tag)
|
tags = append(tags, t.Tags...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
internal/tools/utils.go
Normal file
11
internal/tools/utils.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user