diff --git a/aliases.go b/aliases.go index 7ff27ae..07375e2 100644 --- a/aliases.go +++ b/aliases.go @@ -5,10 +5,15 @@ import ( "fmt" "os" "strings" + "sync" ) type AliasMap map[string]string +var ( + aliasesMutex sync.RWMutex +) + func AliasLoadFile(file string) (AliasMap, error) { aliasMap := make(AliasMap) count := 0 @@ -50,3 +55,18 @@ func AliasLoadFile(file string) (AliasMap, error) { } return aliasMap, nil } + +func LoadAliases(filename string) error { + newAliases, err := AliasLoadFile(filename) + if err != nil { + return err + } + + aliasesMutex.Lock() + defer aliasesMutex.Unlock() + + // Update the aliases map + aliasesList = newAliases + + return nil +} diff --git a/config.go b/config.go index 4c0e65e..4103742 100644 --- a/config.go +++ b/config.go @@ -42,7 +42,7 @@ var ( allowedSenderStr = flagset.String("allowed_sender", "", "Regular expression for valid FROM EMail addresses") allowedRecipStr = flagset.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses") allowedUsers = flagset.String("allowed_users", "", "Path to file with valid users/passwords") - aliasesFile = flagset.String("aliases_file", "", "Path to aliases file") + aliasFile = flagset.String("aliases_file", "", "Path to aliases file") command = flagset.String("command", "", "Path to pipe command") remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers") strictSender = flagset.Bool("strict_sender", false, "Use only SMTP servers with Sender matches to From") @@ -68,12 +68,12 @@ func localAuthRequired() bool { } func setupAliases() { - if *aliasesFile != "" { + if *aliasFile != "" { aliases := make(AliasMap) - aliases, err := AliasLoadFile(*aliasesFile) + aliases, err := AliasLoadFile(*aliasFile) if err != nil { log.Fatal(). - Str("file", *aliasesFile). + Str("file", *aliasFile). Err(err). Msg("cannot load aliases file") } diff --git a/go.mod b/go.mod index 1c5cef3..36a9835 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/decke/smtprelay require ( github.com/DeRuina/timberjack v1.3.9 github.com/chrj/smtpd v0.3.1 + github.com/fsnotify/fsnotify v1.9.0 github.com/google/uuid v1.6.0 github.com/peterbourgon/ff/v3 v3.4.0 github.com/rs/zerolog v1.34.0 diff --git a/go.sum b/go.sum index 75c3e83..fa2351f 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/main.go b/main.go index 0d8735a..e87a4b8 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/chrj/smtpd" + "github.com/fsnotify/fsnotify" "github.com/google/uuid" ) @@ -160,6 +161,7 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error { } // Check for aliases + aliasesMutex.RLock() for i, recipient := range env.Recipients { if alias, exists := aliasesList[recipient]; exists { env.Recipients[i] = alias @@ -169,6 +171,7 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error { Msg("Recipient address aliased") } } + aliasesMutex.RUnlock() logger := log.With(). Str("from", env.Sender). @@ -297,6 +300,71 @@ func getTLSConfig() *tls.Config { } } +func watchAliasFile() { + if *aliasFile == "" { + return + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Error(). + Err(err). + Msg("failed to create file watcher for alias file") + return + } + + go func() { + defer watcher.Close() + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { + log.Info(). + Str("file", event.Name). + Msg("alias file changed, reloading") + + err := LoadAliases(*aliasFile) + if err != nil { + log.Error(). + Str("file", *aliasFile). + Err(err). + Msg("failed to reload alias file") + } else { + log.Info(). + Int("count", len(aliasesList)). + Msg("alias file reloaded successfully") + } + } + + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Error(). + Err(err). + Msg("file watcher error") + } + } + }() + + err = watcher.Add(*aliasFile) + if err != nil { + log.Error(). + Str("file", *aliasFile). + Err(err). + Msg("failed to watch alias file") + } else { + log.Info(). + Str("file", *aliasFile). + Msg("watching alias file for changes") + } +} + func main() { ConfigLoad() @@ -315,6 +383,9 @@ func main() { } } + // Start watching alias file for changes + watchAliasFile() + var servers []*smtpd.Server // Create a server for each desired listen address