split watcher and http server from sender + add golanci config
This commit is contained in:
parent
d831c3e0aa
commit
3c4b740849
134
.golangci.yml
Normal file
134
.golangci.yml
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
linters-settings:
|
||||||
|
# depguard: // Specific for golangci repository
|
||||||
|
# list-type: blacklist
|
||||||
|
# packages:
|
||||||
|
# # logging is allowed only by logutils.Log, logrus
|
||||||
|
# # is allowed to use only in logutils package
|
||||||
|
# - github.com/sirupsen/logrus
|
||||||
|
# packages-with-error-message:
|
||||||
|
# - github.com/sirupsen/logrus: 'logging is allowed only by logutils.Log'
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 50
|
||||||
|
gci:
|
||||||
|
local-prefixes: localenv
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
disabled-checks:
|
||||||
|
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
||||||
|
- ifElseChain
|
||||||
|
- octalLiteral
|
||||||
|
# - whyNoLint
|
||||||
|
- wrapperFunc
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
goimports:
|
||||||
|
local-prefixes: localenv
|
||||||
|
gomnd:
|
||||||
|
settings:
|
||||||
|
mnd:
|
||||||
|
# don't include the "operation" and "assign"
|
||||||
|
checks:
|
||||||
|
- argument
|
||||||
|
- case
|
||||||
|
- condition
|
||||||
|
- return
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
# settings: // Specific for golangci repository
|
||||||
|
# printf:
|
||||||
|
# funcs:
|
||||||
|
# - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||||
|
# - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||||
|
# - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||||
|
# - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||||
|
lll:
|
||||||
|
line-length: 200
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
nolintlint:
|
||||||
|
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||||
|
allow-unused: false # report any unused nolint directives
|
||||||
|
require-explanation: false # don't require an explanation for nolint directives
|
||||||
|
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||||
|
errcheck:
|
||||||
|
check-blank: true
|
||||||
|
exclude-functions:
|
||||||
|
- '(*github.com/gin-gonic/gin.Error).SetType'
|
||||||
|
- '(*github.com/gin-gonic/gin.Context).Error'
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
# - deadcode # deprecated (since v1.49.0)
|
||||||
|
# - depguard
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- exportloopref
|
||||||
|
- exhaustive
|
||||||
|
- funlen
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gomnd
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
# - rowserrcheck # https://github.com/golangci/golangci-lint/issues/2649
|
||||||
|
- staticcheck
|
||||||
|
# - structcheck # https://github.com/golangci/golangci-lint/issues/2649
|
||||||
|
- stylecheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
# - varcheck # deprecated (since v1.49.0)
|
||||||
|
- whitespace
|
||||||
|
# - gochecknoglobals # too many global in ds9
|
||||||
|
|
||||||
|
# don't enable:
|
||||||
|
# - asciicheck
|
||||||
|
# - scopelint
|
||||||
|
# - gocognit
|
||||||
|
# - godot
|
||||||
|
# - godox
|
||||||
|
# - goerr113
|
||||||
|
# - interfacer
|
||||||
|
# - maligned
|
||||||
|
# - nestif
|
||||||
|
# - prealloc
|
||||||
|
# - testpackage
|
||||||
|
# - revive
|
||||||
|
# - wsl
|
||||||
|
|
||||||
|
# issues:
|
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
|
# fix: true
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
skip-dirs: []
|
||||||
@ -10,7 +10,7 @@ import (
|
|||||||
type SMTPConfig struct {
|
type SMTPConfig struct {
|
||||||
User string `validate:"required"`
|
User string `validate:"required"`
|
||||||
Password string `validate:"required"`
|
Password string `validate:"required"`
|
||||||
Url string `validate:"required"`
|
URL string `validate:"required"`
|
||||||
Port string `validate:"required"`
|
Port string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ func NewSMTPConfig(user, password, url, port string) (SMTPConfig, error) {
|
|||||||
config := SMTPConfig{
|
config := SMTPConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Password: password,
|
Password: password,
|
||||||
Url: url,
|
URL: url,
|
||||||
Port: port,
|
Port: port,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +30,6 @@ func NewSMTPConfig(user, password, url, port string) (SMTPConfig, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c SMTPConfig) GetFullUrl() string {
|
func (c SMTPConfig) GetFullURL() string {
|
||||||
return fmt.Sprintf("%s:%s", c.Url, c.Port)
|
return fmt.Sprintf("%s:%s", c.URL, c.Port)
|
||||||
}
|
}
|
||||||
|
|||||||
27
mail/mail.go
27
mail/mail.go
@ -20,30 +20,29 @@ type Email struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FromJSON(path string) (Email, error) {
|
func FromJSON(path string) (Email, error) {
|
||||||
var mail Email
|
var email Email
|
||||||
|
|
||||||
content, err := os.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mail, fmt.Errorf("%w, unable to read the file: %s", err, path)
|
return email, fmt.Errorf("%w, unable to read the file: %s", err, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(content, &mail); err != nil {
|
if err := json.Unmarshal(content, &email); err != nil {
|
||||||
return mail, err
|
return email, err
|
||||||
}
|
}
|
||||||
|
|
||||||
validate = validator.New(validator.WithRequiredStructEnabled())
|
if err := email.Validate(); err != nil {
|
||||||
if err := validate.Struct(mail); err != nil {
|
return email, err
|
||||||
return mail, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mail, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Email) GetReceivers() []string {
|
func (e *Email) GetReceivers() []string {
|
||||||
return strings.Split(e.Receivers, ",")
|
return strings.Split(e.Receivers, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Email) Generate() []byte {
|
func (e *Email) Generate() []byte {
|
||||||
mail := fmt.Sprintf(
|
mail := fmt.Sprintf(
|
||||||
"To: %s\nFrom: %s\nContent-Type: text/html;charset=utf-8\nSubject: %s\n\n%s",
|
"To: %s\nFrom: %s\nContent-Type: text/html;charset=utf-8\nSubject: %s\n\n%s",
|
||||||
e.Receivers,
|
e.Receivers,
|
||||||
@ -53,3 +52,11 @@ func (e Email) Generate() []byte {
|
|||||||
)
|
)
|
||||||
return []byte(mail)
|
return []byte(mail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Email) Validate() error {
|
||||||
|
validate = validator.New(validator.WithRequiredStructEnabled())
|
||||||
|
if err := validate.Struct(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -56,5 +56,4 @@ func TestFromJson(t *testing.T) {
|
|||||||
|
|
||||||
assert.Contains(t, err.Error(), "validation for 'Sender'")
|
assert.Contains(t, err.Error(), "validation for 'Sender'")
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
main.go
12
main.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
cfg "mailsrv/config"
|
cfg "mailsrv/config"
|
||||||
@ -12,17 +13,20 @@ import (
|
|||||||
ini "gopkg.in/ini.v1"
|
ini "gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultOutboxPath string = "outbox"
|
const (
|
||||||
|
DefaultOutboxPath string = "outbox"
|
||||||
|
DefaultPermissions fs.FileMode = 0750
|
||||||
|
)
|
||||||
|
|
||||||
var iniPath = kingpin.Arg("ini", ".ini file path").Required().String()
|
var iniPath = kingpin.Arg("ini", ".ini file path").Required().String()
|
||||||
|
|
||||||
func LoadIni(iniPath string) (*ini.File, error) {
|
func LoadIni(iniPath string) (*ini.File, error) {
|
||||||
ini, err := ini.Load(iniPath)
|
iniFile, err := ini.Load(iniPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ini, nil
|
return iniFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadSMTPConfig collects mandatory SMTP parameters to send an e-mail from the `.ini` file
|
// LoadSMTPConfig collects mandatory SMTP parameters to send an e-mail from the `.ini` file
|
||||||
@ -51,7 +55,7 @@ func GetOutboxPath(iniFile *ini.File) (string, error) {
|
|||||||
outboxPath = DefaultOutboxPath
|
outboxPath = DefaultOutboxPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(outboxPath, 0750); err != nil && !os.IsExist(err) {
|
if err := os.MkdirAll(outboxPath, DefaultPermissions); err != nil && !os.IsExist(err) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,51 +1,39 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
cfg "mailsrv/config"
|
cfg "mailsrv/config"
|
||||||
"mailsrv/mail"
|
"mailsrv/mail"
|
||||||
"mailsrv/runtime"
|
"mailsrv/runtime"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
TickerInterval time.Duration = 10 * time.Second
|
|
||||||
JSONSuffix string = ".json"
|
|
||||||
ErrorSuffix string = ".err"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Sender struct {
|
type Sender struct {
|
||||||
smtpConfig cfg.SMTPConfig
|
auth smtp.Auth
|
||||||
// fetch this directory to collect `.json` e-mail format
|
smtpURL string
|
||||||
|
|
||||||
outboxPath string
|
outboxPath string
|
||||||
queue *runtime.Queue
|
|
||||||
|
queue *runtime.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(config cfg.SMTPConfig, outboxPath string) Sender {
|
func NewSender(config cfg.SMTPConfig, outboxPath string) Sender {
|
||||||
return Sender{
|
return Sender{
|
||||||
smtpConfig: config,
|
auth: smtp.PlainAuth("", config.User, config.Password, config.URL),
|
||||||
|
smtpURL: config.GetFullURL(),
|
||||||
outboxPath: outboxPath,
|
outboxPath: outboxPath,
|
||||||
queue: runtime.NewQueue(),
|
queue: runtime.NewQueue(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Sender) SendMail(email mail.Email) error {
|
func (s Sender) SendMail(email *mail.Email) error {
|
||||||
auth := smtp.PlainAuth("", s.smtpConfig.User, s.smtpConfig.Password, s.smtpConfig.Url)
|
if err := smtp.SendMail(s.smtpURL, s.auth, email.Sender, email.GetReceivers(), email.Generate()); err != nil {
|
||||||
log.Debug().Msg("SMTP authentication succeed")
|
|
||||||
|
|
||||||
if err := smtp.SendMail(s.smtpConfig.GetFullUrl(), auth, email.Sender, email.GetReceivers(), email.Generate()); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,80 +41,6 @@ func (s Sender) SendMail(email mail.Email) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Sender) mailHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
content, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("unable to read request body")
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var mail mail.Email
|
|
||||||
if err := json.Unmarshal(content, &mail); err != nil {
|
|
||||||
log.Err(err).Msg("unable to deserialized request body into mail")
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.queue.Add(mail)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Sender) runHTTPserver() {
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/mail", s.mailHandler)
|
|
||||||
|
|
||||||
if err := http.ListenAndServe(":1212", mux); err != nil {
|
|
||||||
log.Err(err).Msg("http server stops listening")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
log.Debug().Str("action", "retrieving json e-mail format...").Str("path", s.outboxPath)
|
|
||||||
|
|
||||||
files, err := os.ReadDir(s.outboxPath)
|
|
||||||
if err != nil && !os.IsExist(err) {
|
|
||||||
log.Err(err).Msg("outbox directory does not exist")
|
|
||||||
s.queue.Shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filename := file.Name()
|
|
||||||
if !strings.HasSuffix(filename, JSONSuffix) {
|
|
||||||
log.Debug().Str("filename", filename).Msg("incorrect suffix")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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 iterates over the queue and send email.
|
// processNextEmail iterates over the queue and send email.
|
||||||
func (s Sender) processNextEmail() bool {
|
func (s Sender) processNextEmail() bool {
|
||||||
item, quit := s.queue.Get()
|
item, quit := s.queue.Get()
|
||||||
@ -141,7 +55,7 @@ func (s Sender) processNextEmail() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.SendMail(email); err != nil {
|
if err := s.SendMail(&email); err != nil {
|
||||||
log.Err(err).Msg("unable to send the email")
|
log.Err(err).Msg("unable to send the email")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,26 +83,44 @@ func (s Sender) run() <-chan struct{} {
|
|||||||
return chQueue
|
return chQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run launches the queue processing and the outbox watcher.
|
// Run launches the queue processing, the outbox watcher and the HTTP server.
|
||||||
// It catches `SIGINT` and `SIGTERM` to properly stopped the queue.
|
// It catches `SIGINT` and `SIGTERM` to properly stopped the queue and the services.
|
||||||
func (s Sender) Run() {
|
func (s Sender) Run() {
|
||||||
log.Info().Msg("sender service is running")
|
ctx, fnCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
chSignal := make(chan os.Signal, 1)
|
chSignal := make(chan os.Signal, 1)
|
||||||
signal.Notify(chSignal, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(chSignal, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
s.watchOutbox()
|
|
||||||
chQueue := s.run()
|
chQueue := s.run()
|
||||||
|
|
||||||
go s.runHTTPserver()
|
server := NewServer(ctx, "1212", s.queue)
|
||||||
|
server.Serve()
|
||||||
|
|
||||||
|
watcher := NewDirectoryWatch(ctx, s.outboxPath, s.queue)
|
||||||
|
watcher.Watch()
|
||||||
|
|
||||||
|
log.Info().Msg("sender service is running...")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-chSignal:
|
case <-chSignal:
|
||||||
log.Warn().Msg("stop signal received, stopping e-mail queue...")
|
log.Warn().Msg("stop signal received, stopping...")
|
||||||
s.queue.Shutdown()
|
fnCancel()
|
||||||
case <-chQueue:
|
case <-watcher.Done():
|
||||||
log.Info().Msg("e-mail queue stopped successfully")
|
log.Warn().Msg("watcher is done, stopping...")
|
||||||
|
fnCancel()
|
||||||
|
case <-server.Done():
|
||||||
|
log.Warn().Msg("server is done, stopping...")
|
||||||
|
fnCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("sender service stopped successfully")
|
<-server.Done()
|
||||||
|
log.Info().Msg("http server stopped successfully")
|
||||||
|
|
||||||
|
<-watcher.Done()
|
||||||
|
log.Info().Msg("watcher stopped successfully")
|
||||||
|
|
||||||
|
s.queue.Shutdown()
|
||||||
|
<-chQueue
|
||||||
|
|
||||||
|
log.Info().Msg("mailsrv stopped gracefully")
|
||||||
}
|
}
|
||||||
|
|||||||
99
services/server.go
Normal file
99
services/server.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mailsrv/mail"
|
||||||
|
"mailsrv/runtime"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPServer interface {
|
||||||
|
Serve()
|
||||||
|
Done() <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
fnCancel context.CancelFunc
|
||||||
|
|
||||||
|
port string
|
||||||
|
queue *runtime.Queue
|
||||||
|
|
||||||
|
chDone chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(ctx context.Context, port string, queue *runtime.Queue) Server {
|
||||||
|
ctxChild, fnCancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return Server{
|
||||||
|
ctx: ctxChild,
|
||||||
|
fnCancel: fnCancel,
|
||||||
|
port: port,
|
||||||
|
queue: queue,
|
||||||
|
chDone: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Done() <-chan struct{} {
|
||||||
|
return s.chDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Serve() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/mail", s.handler)
|
||||||
|
|
||||||
|
log.Info().Str("port", s.port).Msg("http server is listening...")
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%s", s.port),
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-s.ctx.Done()
|
||||||
|
if err := server.Shutdown(s.ctx); err != nil {
|
||||||
|
log.Err(err).Msg("bad server shutdown")
|
||||||
|
}
|
||||||
|
s.chDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
log.Err(err).Msg("http server stops listening")
|
||||||
|
s.fnCancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
content, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("unable to read request body")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var email mail.Email
|
||||||
|
if err := json.Unmarshal(content, &email); err != nil {
|
||||||
|
log.Err(err).Msg("unable to deserialized request body into mail")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := email.Validate(); err != nil {
|
||||||
|
log.Err(err).Msg("email validation failed")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.queue.Add(email)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
100
services/watcher.go
Normal file
100
services/watcher.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"mailsrv/mail"
|
||||||
|
"mailsrv/runtime"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TickerInterval time.Duration = 10 * time.Second
|
||||||
|
JSONSuffix string = ".json"
|
||||||
|
ErrorSuffix string = ".err"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Watcher interface {
|
||||||
|
Watch()
|
||||||
|
Done() <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryWatch watches a directory every `tick` interval and collect email files.
|
||||||
|
type DirectoryWatch struct {
|
||||||
|
ctx context.Context
|
||||||
|
fnCancel context.CancelFunc
|
||||||
|
|
||||||
|
outboxPath string
|
||||||
|
queue *runtime.Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirectoryWatch(ctx context.Context, outboxPath string, queue *runtime.Queue) DirectoryWatch {
|
||||||
|
ctxChild, fnCancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return DirectoryWatch{
|
||||||
|
ctx: ctxChild,
|
||||||
|
fnCancel: fnCancel,
|
||||||
|
outboxPath: outboxPath,
|
||||||
|
queue: queue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dw DirectoryWatch) Done() <-chan struct{} {
|
||||||
|
return dw.ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch reads the `outbox` directory every `TickInterval` and put JSON format e-mail in the queue.
|
||||||
|
func (dw DirectoryWatch) Watch() {
|
||||||
|
log.Info().Str("outbox", dw.outboxPath).Msg("watching outbox directory...")
|
||||||
|
|
||||||
|
ticker := time.NewTicker(TickerInterval)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-dw.Done():
|
||||||
|
log.Err(dw.ctx.Err()).Msg("context done")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
log.Debug().Str("action", "retrieving json e-mail format...").Str("path", dw.outboxPath)
|
||||||
|
|
||||||
|
files, err := os.ReadDir(dw.outboxPath)
|
||||||
|
if err != nil && !os.IsExist(err) {
|
||||||
|
log.Err(err).Msg("outbox directory does not exist")
|
||||||
|
dw.fnCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
if !strings.HasSuffix(filename, JSONSuffix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPath := path.Join(dw.outboxPath, filename)
|
||||||
|
email, err := mail.FromJSON(emailPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("path", emailPath).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", emailPath, ErrorSuffix)
|
||||||
|
if err := os.Rename(emailPath, newPath); err != nil {
|
||||||
|
log.Err(err).Str("path", emailPath).Str("new path", newPath).Msg("unable to rename bad JSON email path")
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
email.Path = emailPath
|
||||||
|
dw.queue.Add(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user