diff --git a/.gitignore b/.gitignore index 332ba63..534271a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -mailsrv +builds +outbox + +*.ini diff --git a/Makefile b/Makefile index b8626c2..9c85ebc 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,20 @@ .DEFAULT_GOAL := build -fmt: +.DEFAULT_GOAL := run + +BIN_NAME := mailsrv +ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) +BUILD_DIR := $(ROOT_DIR)builds + +build: format lint + @mkdir -p $(BUILD_DIR) + @go build -o $(BUILD_DIR)/$(BIN_NAME) main.go + +format: go fmt ./... -.PHONY:fmt -lint: fmt - golint ./... -.PHONY:lint - -vet: fmt - go vet ./... - shadow ./... -.PHONY:vet - -build: - go build -o mailsrv -.PHONY:build - -build-check: vet - go build -o mailsrv -.PHONY:build +lint: + golangci-lint run --fix test: go test ./... -.PHONY:test diff --git a/go.mod b/go.mod index ad11d7b..b374ca0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module mailsrv -go 1.17 +go 1.20 require ( github.com/alecthomas/kingpin/v2 v2.3.2 diff --git a/go.sum b/go.sum index ae9e8cb..93116ed 100644 --- a/go.sum +++ b/go.sum @@ -5,7 +5,6 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -18,13 +17,8 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -34,6 +28,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mail/mail.go b/mail/mail.go index dfd8bf6..3b6bab4 100644 --- a/mail/mail.go +++ b/mail/mail.go @@ -3,11 +3,12 @@ package mail import ( "encoding/json" "fmt" - "io/ioutil" + "os" "strings" ) type Email struct { + Path string Sender string `json:"sender"` Receivers []string `json:"receivers"` Subject string `json:"subject"` @@ -26,9 +27,9 @@ func NewEmail(sender string, receivers []string, subject, content string) Email func FromJSON(path string) (Email, error) { var mail Email - content, err := ioutil.ReadFile(path) + content, err := os.ReadFile(path) if err != nil { - return mail, err + return mail, fmt.Errorf("%w, unable to read the file: %s", err, path) } if err := json.Unmarshal(content, &mail); err != nil { diff --git a/services/sender.go b/services/sender.go index 1ce0d7d..5e639b0 100644 --- a/services/sender.go +++ b/services/sender.go @@ -51,14 +51,14 @@ func (s Sender) SendMail(email mail.Email) error { return nil } -// watchOutbox reads the `outbox` directory every `TickInterval` and put JSON format e-mail in the queue +// watchOutbox reads the `outbox` directory every `TickInterval` and put JSON format e-mail in the queue. func (s Sender) watchOutbox() { log.Info().Str("outbox", s.outboxPath).Msg("start watching outbox directory") ticker := time.NewTicker(TickerInterval) go func() { - for _ = range ticker.C { + for range ticker.C { log.Debug().Str("action", "retrieving json e-mail format...").Str("path", s.outboxPath) files, err := os.ReadDir(s.outboxPath) @@ -69,18 +69,34 @@ func (s Sender) watchOutbox() { for _, file := range files { filename := file.Name() - if strings.HasSuffix(filename, JSONSuffix) { - s.queue.Add(path.Join(s.outboxPath, filename)) + if !strings.HasSuffix(filename, JSONSuffix) { + log.Debug().Str("filename", filename).Msg("incorrect suffix") continue } - log.Debug().Str("filename", filename).Msg("incorrect suffix") + path := path.Join(s.outboxPath, filename) + email, err := mail.FromJSON(path) + + if err != nil { + log.Err(err).Str("path", path).Msg("unable to parse JSON email") + + // if JSON parsing failed the `path` is renamed with an error suffix to not watch it again + newPath := fmt.Sprintf("%s%s", path, ErrorSuffix) + if err := os.Rename(path, newPath); err != nil { + log.Err(err).Str("path", path).Str("new path", newPath).Msg("unable to rename bad JSON email path") + } + + continue + } + + email.Path = path + s.queue.Add(email) } } }() } -// processNextEmail loops over the queue and send email +// processNextEmail iterates over the queue and send email. func (s Sender) processNextEmail() bool { item, quit := s.queue.Get() if quit { @@ -88,66 +104,56 @@ func (s Sender) processNextEmail() bool { } defer s.queue.Done(item) - path, ok := item.(string) + email, ok := item.(mail.Email) if !ok { log.Error().Any("item", item).Msg("unable to cast queue item into mail.Email") return true } - email, err := mail.FromJSON(path) - if err != nil { - log.Err(err).Str("path", path).Msg("unable to parse JSON email") - - // if JSON parsing failed the `path` is renamed with an error suffix to avoid enqueued it again - newPath := fmt.Sprintf("%s%s", path, ErrorSuffix) - if err := os.Rename(path, newPath); err != nil { - log.Err(err).Str("path", path).Str("new path", newPath).Msg("unable to rename bad JSON email path") - s.queue.Shutdown() - } - return true + if err := s.SendMail(email); err != nil { + log.Err(err).Msg("unable to send the email") } - // whatever the return, the email will be not enqueued again - s.SendMail(email) - - if err := os.Remove(path); err != nil { - // this is a fatal error, can't send same e-mail indefinitely - if !os.IsExist(err) { - log.Err(err).Str("path", path).Msg("unable to remove the JSON email") - s.queue.Shutdown() + if path := email.Path; path != "" { + if err := os.Remove(path); err != nil { + // this is a fatal error, can't send same e-mail indefinitely + if !os.IsExist(err) { + log.Err(err).Str("path", path).Msg("unable to remove the JSON email") + s.queue.Shutdown() + } } } return true } -// run starts processing the queue +// run starts processing the queue. func (s Sender) run() <-chan struct{} { - queueCh := make(chan struct{}) + chQueue := make(chan struct{}) go func() { for s.processNextEmail() { } - queueCh <- struct{}{} + chQueue <- struct{}{} }() - return queueCh + return chQueue } -// Run launches the queue processing and the outbox watcher -// catches `SIGINT` and `SIGTERM` to properly stopped the queue +// Run launches the queue processing and the outbox watcher. +// It catches `SIGINT` and `SIGTERM` to properly stopped the queue. func (s Sender) Run() { log.Info().Msg("sender service is running") - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + chSignal := make(chan os.Signal, 1) + signal.Notify(chSignal, os.Interrupt, syscall.SIGTERM) s.watchOutbox() - queueCh := s.run() + chQueue := s.run() select { - case <-sigCh: + case <-chSignal: log.Warn().Msg("stop signal received, stopping e-mail queue...") s.queue.Shutdown() - case <-queueCh: + case <-chQueue: log.Info().Msg("e-mail queue stopped successfully") }