diff options
| -rw-r--r-- | config/config.go | 36 | ||||
| -rw-r--r-- | config/constants.go | 10 | ||||
| -rw-r--r-- | config/functions.go | 58 | ||||
| -rw-r--r-- | config/types.go | 60 | ||||
| -rw-r--r-- | enums/mailbox.go | 8 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 16 | ||||
| -rw-r--r-- | messages/config.go | 10 | ||||
| -rw-r--r-- | messages/logger.go | 5 | ||||
| -rw-r--r-- | messages/toml.go | 5 | ||||
| -rw-r--r-- | utils/collections/types.go | 3 | ||||
| -rw-r--r-- | utils/errors/errors.go | 14 | ||||
| -rw-r--r-- | utils/logger/constants.go | 25 | ||||
| -rw-r--r-- | utils/logger/functions.go | 53 | ||||
| -rw-r--r-- | utils/logger/logger.go | 83 | ||||
| -rw-r--r-- | utils/logger/types.go | 11 | ||||
| -rw-r--r-- | utils/toml/defaults.go | 64 | ||||
| -rw-r--r-- | utils/toml/load.go | 21 | ||||
| -rw-r--r-- | utils/toml/parse.go | 40 | ||||
| -rw-r--r-- | utils/toml/unmarshal.go | 11 |
20 files changed, 543 insertions, 0 deletions
diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..358fe72 --- /dev/null +++ b/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "dove/messages" + "dove/utils/logger" +) + +var ( + IMAP imap + Mailbox mailbox + POP3 pop3 + Server server + SMTP smtp +) + +var ( + DataDir string + DevMode bool +) + +func init() { + DevMode = isDevelopmentMode() + osConfigDirectory := resolveOSConfigDirectory() + DataDir = resolveDataDirectory(DevMode, osConfigDirectory) + + configFilePath := resolveConfigFilePath(DevMode, osConfigDirectory) + if loadError := loadConfigFile(configFilePath); loadError != nil { + logger.Fatalf(LOG_PREFIX, messages.ConfigFileLoadFailed, loadError) + } + + for _, section := range []any{&Server, &SMTP, &IMAP, &POP3, &Mailbox} { + if parseError := parseSection(section); parseError != nil { + logger.Fatalf(LOG_PREFIX, messages.ConfigSectionParseFailed, parseError) + } + } +} diff --git a/config/constants.go b/config/constants.go new file mode 100644 index 0000000..14b6276 --- /dev/null +++ b/config/constants.go @@ -0,0 +1,10 @@ +package config + +const ( + APPLICATION_DIRECTORY = "dove" + CONFIG_FILE_NAME = "config.toml" + CURRENT_DIRECTORY = "." + GO_BUILD_ALT_INDICATOR = "go_build" + GO_BUILD_PATH_INDICATOR = "go-build" + LOG_PREFIX = "Config" +) diff --git a/config/functions.go b/config/functions.go new file mode 100644 index 0000000..58ab717 --- /dev/null +++ b/config/functions.go @@ -0,0 +1,58 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + + "dove/utils/toml" +) + +func isDevelopmentMode() bool { + executablePath, pathError := os.Executable() + if pathError != nil { + return false + } + + return strings.Contains(executablePath, GO_BUILD_PATH_INDICATOR) || + strings.Contains(executablePath, GO_BUILD_ALT_INDICATOR) +} + +func resolveOSConfigDirectory() string { + configDirectory, directoryError := os.UserConfigDir() + if directoryError != nil { + return CURRENT_DIRECTORY + } + + return filepath.Join(configDirectory, APPLICATION_DIRECTORY) +} + +func resolveConfigFilePath(developmentMode bool, osConfigDirectory string) string { + switch developmentMode { + case true: + return filepath.Join(CURRENT_DIRECTORY, CONFIG_FILE_NAME) + default: + return filepath.Join(osConfigDirectory, CONFIG_FILE_NAME) + } +} + +func resolveDataDirectory(developmentMode bool, osConfigDirectory string) string { + switch developmentMode { + case true: + return CURRENT_DIRECTORY + default: + return osConfigDirectory + } +} + +func loadConfigFile(configFilePath string) error { + if _, statError := os.Stat(configFilePath); statError != nil { + return nil + } + + return toml.LoadFile(configFilePath) +} + +func parseSection(target any) error { + return toml.Parse(target) +} diff --git a/config/types.go b/config/types.go new file mode 100644 index 0000000..ab5a569 --- /dev/null +++ b/config/types.go @@ -0,0 +1,60 @@ +package config + +import "dove/enums" + +type server struct { + Host string `toml:"host" default:"0.0.0.0"` + Port int `toml:"port" default:"8080"` + Debug bool `toml:"debug" default:"false"` + Username string `toml:"username"` + Password string `toml:"password"` +} + +type smtp struct { + Host string `toml:"host" default:"0.0.0.0"` + Port int `toml:"port" default:"5025"` + SMTPSPort int `toml:"smtps_port" default:"5465"` + StartTLSPort int `toml:"starttls_port" default:"5587"` + MaxMessageSize int `toml:"max_message_size" default:"26214400"` + AuthRequired bool `toml:"auth_required" default:"false"` + Username string `toml:"username"` + Password string `toml:"password"` + TLSEnabled bool `toml:"tls_enabled" default:"false"` + TLSCertPath string `toml:"tls_cert"` + TLSKeyPath string `toml:"tls_key"` + RelayEnabled bool `toml:"relay_enabled" default:"false"` + RelayHost string `toml:"relay_host"` + RelayPort int `toml:"relay_port" default:"587"` + RelayUsername string `toml:"relay_username"` + RelayPassword string `toml:"relay_password"` + RelayStartTLS bool `toml:"relay_starttls" default:"true"` +} + +type imap struct { + Host string `toml:"host" default:"0.0.0.0"` + Port int `toml:"port" default:"5143"` + IMAPSPort int `toml:"imaps_port" default:"5993"` + AuthRequired bool `toml:"auth_required" default:"false"` + Username string `toml:"username"` + Password string `toml:"password"` + TLSEnabled bool `toml:"tls_enabled" default:"false"` + TLSCertPath string `toml:"tls_cert"` + TLSKeyPath string `toml:"tls_key"` +} + +type pop3 struct { + Host string `toml:"host" default:"0.0.0.0"` + Port int `toml:"port" default:"5110"` + POP3SPort int `toml:"pop3s_port" default:"5995"` + AuthRequired bool `toml:"auth_required" default:"false"` + Username string `toml:"username"` + Password string `toml:"password"` + TLSEnabled bool `toml:"tls_enabled" default:"false"` + TLSCertPath string `toml:"tls_cert"` + TLSKeyPath string `toml:"tls_key"` +} + +type mailbox struct { + Mode enums.MailboxMode `toml:"mode" default:"registered"` +} + diff --git a/enums/mailbox.go b/enums/mailbox.go new file mode 100644 index 0000000..24b2f4b --- /dev/null +++ b/enums/mailbox.go @@ -0,0 +1,8 @@ +package enums + +type MailboxMode string + +const ( + Registered MailboxMode = "registered" + Catchall MailboxMode = "catchall" +) @@ -0,0 +1,10 @@ +module dove + +go 1.25.0 + +require ( + github.com/pelletier/go-toml/v2 v2.2.4 + go.uber.org/zap v1.27.1 +) + +require go.uber.org/multierr v1.10.0 // indirect @@ -0,0 +1,16 @@ +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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/messages/config.go b/messages/config.go new file mode 100644 index 0000000..fe44f45 --- /dev/null +++ b/messages/config.go @@ -0,0 +1,10 @@ +package messages + +const ( + ConfigFileReadFailed = "failed to read config file %s: %s" + ConfigFileParseFailed = "failed to parse config file %s: %s" + ConfigSectionNotFound = "config section '%s' not found" + ConfigSectionInvalid = "config section '%s' has invalid data" + ConfigFileLoadFailed = "failed to load config: %v" + ConfigSectionParseFailed = "failed to parse config section: %v" +) diff --git a/messages/logger.go b/messages/logger.go new file mode 100644 index 0000000..46c12e6 --- /dev/null +++ b/messages/logger.go @@ -0,0 +1,5 @@ +package messages + +const ( + LoggerNotInitialized = "logger.Init() was not called" +) diff --git a/messages/toml.go b/messages/toml.go new file mode 100644 index 0000000..ee4cbfd --- /dev/null +++ b/messages/toml.go @@ -0,0 +1,5 @@ +package messages + +const ( + ParseTargetMustBeStructPointer = "parse target must be a pointer to a struct" +) diff --git a/utils/collections/types.go b/utils/collections/types.go new file mode 100644 index 0000000..c75d1ea --- /dev/null +++ b/utils/collections/types.go @@ -0,0 +1,3 @@ +package collections + +type Record map[string]any diff --git a/utils/errors/errors.go b/utils/errors/errors.go new file mode 100644 index 0000000..015356a --- /dev/null +++ b/utils/errors/errors.go @@ -0,0 +1,14 @@ +package errors + +import ( + "errors" + "fmt" +) + +func Error(message string, arguments ...any) error { + if len(arguments) == 0 { + return errors.New(message) + } + + return fmt.Errorf(message, arguments...) +} diff --git a/utils/logger/constants.go b/utils/logger/constants.go new file mode 100644 index 0000000..aba37ae --- /dev/null +++ b/utils/logger/constants.go @@ -0,0 +1,25 @@ +package logger + +const ( + ANSI_RESET = "\033[0m" +) + +const ( + LEVEL_COLOR_DEBUG = "\033[35mDEBUG \033[0m" + LEVEL_COLOR_ERROR = "\033[31mERROR \033[0m" + LEVEL_COLOR_INFO = "\033[34mINFO \033[0m" + LEVEL_COLOR_WARN = "\033[33mWARN \033[0m" +) + +const ( + MESSAGE_COLOR_DEBUG = "\033[90m" + MESSAGE_COLOR_ERROR = "\033[31m" + MESSAGE_COLOR_INFO = "\033[97m" + MESSAGE_COLOR_SUCCESS = "\033[32m" + MESSAGE_COLOR_WARN = "\033[33m" +) + +const ( + PREFIX_COLOR = "\033[36m" + PREFIX_WIDTH = 15 +) diff --git a/utils/logger/functions.go b/utils/logger/functions.go new file mode 100644 index 0000000..7bd9850 --- /dev/null +++ b/utils/logger/functions.go @@ -0,0 +1,53 @@ +package logger + +import ( + "fmt" + "strings" + + "go.uber.org/zap/zapcore" +) + +func formatLevel(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + encoder.AppendString(LEVEL_COLOR_DEBUG) + case zapcore.WarnLevel: + encoder.AppendString(LEVEL_COLOR_WARN) + case zapcore.ErrorLevel: + encoder.AppendString(LEVEL_COLOR_ERROR) + default: + encoder.AppendString(LEVEL_COLOR_INFO) + } +} + +func formatPrefix(prefix string) string { + if prefix == "" { + return "" + } + + padding := "" + if len(prefix) < PREFIX_WIDTH { + padding = strings.Repeat(" ", PREFIX_WIDTH-len(prefix)) + } + + return PREFIX_COLOR + "[" + prefix + "]" + ANSI_RESET + padding +} + +func colorizeMessage(level logLevel, message string) string { + switch level { + case levelDebug: + return MESSAGE_COLOR_DEBUG + message + ANSI_RESET + case levelWarn: + return MESSAGE_COLOR_WARN + message + ANSI_RESET + case levelError: + return MESSAGE_COLOR_ERROR + message + ANSI_RESET + case levelSuccess: + return MESSAGE_COLOR_SUCCESS + message + ANSI_RESET + default: + return MESSAGE_COLOR_INFO + message + ANSI_RESET + } +} + +func buildFullMessage(level logLevel, prefix string, message any) string { + return formatPrefix(prefix) + colorizeMessage(level, fmt.Sprint(message)) +} diff --git a/utils/logger/logger.go b/utils/logger/logger.go new file mode 100644 index 0000000..b1d1809 --- /dev/null +++ b/utils/logger/logger.go @@ -0,0 +1,83 @@ +package logger + +import ( + "fmt" + "os" + + "dove/messages" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + instance *zap.Logger + atomicLevel zap.AtomicLevel +) + +func Init() { + atomicLevel = zap.NewAtomicLevelAt(zapcore.InfoLevel) + + encoderConfig := zapcore.EncoderConfig{ + LevelKey: "level", + MessageKey: "msg", + LineEnding: "\n", + EncodeLevel: formatLevel, + } + + encoder := zapcore.NewConsoleEncoder(encoderConfig) + stdoutSink := zapcore.AddSync(os.Stdout) + stderrSink := zapcore.AddSync(os.Stderr) + + core := zapcore.NewTee( + zapcore.NewCore(encoder, stdoutSink, zap.LevelEnablerFunc(func(level zapcore.Level) bool { + return level < zapcore.WarnLevel && atomicLevel.Enabled(level) + })), + zapcore.NewCore(encoder, stderrSink, zap.LevelEnablerFunc(func(level zapcore.Level) bool { + return level >= zapcore.WarnLevel && atomicLevel.Enabled(level) + })), + ) + + instance = zap.New(core, zap.AddCaller()) +} + +func SetDebug(enabled bool) { + if enabled { + atomicLevel.SetLevel(zapcore.DebugLevel) + } else { + atomicLevel.SetLevel(zapcore.InfoLevel) + } +} + +func Debugf(prefix string, format string, arguments ...any) { + emit(levelDebug, zapcore.DebugLevel, prefix, fmt.Sprintf(format, arguments...)) +} + +func Infof(prefix string, format string, arguments ...any) { + emit(levelInfo, zapcore.InfoLevel, prefix, fmt.Sprintf(format, arguments...)) +} + +func Successf(prefix string, format string, arguments ...any) { + emit(levelSuccess, zapcore.InfoLevel, prefix, fmt.Sprintf(format, arguments...)) +} + +func Warnf(prefix string, format string, arguments ...any) { + emit(levelWarn, zapcore.WarnLevel, prefix, fmt.Sprintf(format, arguments...)) +} + +func Errorf(prefix string, format string, arguments ...any) { + emit(levelError, zapcore.ErrorLevel, prefix, fmt.Sprintf(format, arguments...)) +} + +func Fatalf(prefix string, format string, arguments ...any) { + emit(levelError, zapcore.ErrorLevel, prefix, fmt.Sprintf(format, arguments...)) + os.Exit(1) +} + +func emit(levelLabel logLevel, zapLevel zapcore.Level, prefix string, message any) { + if instance == nil { + panic(messages.LoggerNotInitialized) + } + + instance.Log(zapLevel, buildFullMessage(levelLabel, prefix, message)) +} diff --git a/utils/logger/types.go b/utils/logger/types.go new file mode 100644 index 0000000..fce392d --- /dev/null +++ b/utils/logger/types.go @@ -0,0 +1,11 @@ +package logger + +type logLevel string + +const ( + levelDebug logLevel = "debug" + levelInfo logLevel = "info" + levelWarn logLevel = "warn" + levelError logLevel = "error" + levelSuccess logLevel = "success" +) diff --git a/utils/toml/defaults.go b/utils/toml/defaults.go new file mode 100644 index 0000000..e9ef032 --- /dev/null +++ b/utils/toml/defaults.go @@ -0,0 +1,64 @@ +package toml + +import ( + "reflect" + "strconv" +) + +func ApplyDefaults(target any) { + targetValue := reflect.ValueOf(target) + + if targetValue.Kind() != reflect.Pointer || targetValue.Elem().Kind() != reflect.Struct { + return + } + + applyStructDefaults(targetValue.Elem()) +} + +func applyStructDefaults(structValue reflect.Value) { + structType := structValue.Type() + + for fieldIndex := range structType.NumField() { + fieldValue := structValue.Field(fieldIndex) + fieldDescriptor := structType.Field(fieldIndex) + + if !fieldValue.CanSet() { + continue + } + + if fieldValue.Kind() == reflect.Struct { + applyStructDefaults(fieldValue) + continue + } + + defaultValue := fieldDescriptor.Tag.Get("default") + if defaultValue == "" { + continue + } + + setDefaultValue(fieldValue, defaultValue) + } +} + +func setDefaultValue(field reflect.Value, defaultValue string) { + if !isZeroValue(field) { + return + } + + switch field.Kind() { + case reflect.String: + field.SetString(defaultValue) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if parsed, parseError := strconv.ParseInt(defaultValue, 10, 64); parseError == nil { + field.SetInt(parsed) + } + case reflect.Bool: + if parsed, parseError := strconv.ParseBool(defaultValue); parseError == nil { + field.SetBool(parsed) + } + } +} + +func isZeroValue(field reflect.Value) bool { + return field.IsZero() +} diff --git a/utils/toml/load.go b/utils/toml/load.go new file mode 100644 index 0000000..a0cab4b --- /dev/null +++ b/utils/toml/load.go @@ -0,0 +1,21 @@ +package toml + +import ( + "os" + + "dove/messages" + "dove/utils/collections" + "dove/utils/errors" +) + +var loadedData collections.Record + +func LoadFile(filePath string) error { + fileContent, readError := os.ReadFile(filePath) + if readError != nil { + return errors.Error(messages.ConfigFileReadFailed, filePath, readError.Error()) + } + + loadedData = make(collections.Record) + return unmarshalContent(fileContent, &loadedData) +} diff --git a/utils/toml/parse.go b/utils/toml/parse.go new file mode 100644 index 0000000..7e2e6f2 --- /dev/null +++ b/utils/toml/parse.go @@ -0,0 +1,40 @@ +package toml + +import ( + "reflect" + "strings" + + "dove/messages" + "dove/utils/errors" +) + +func Parse(target any) error { + targetValue := reflect.ValueOf(target) + if targetValue.Kind() != reflect.Pointer || targetValue.Elem().Kind() != reflect.Struct { + return errors.Error(messages.ParseTargetMustBeStructPointer) + } + + ApplyDefaults(target) + + if loadedData == nil { + return nil + } + + sectionName := resolveSectionName(targetValue) + sectionData, exists := loadedData[sectionName] + if !exists { + return nil + } + + sectionBytes, marshalError := marshalSection(sectionData) + if marshalError != nil { + return errors.Error(messages.ConfigSectionInvalid, sectionName) + } + + return unmarshalContent(sectionBytes, target) +} + +func resolveSectionName(targetValue reflect.Value) string { + typeName := targetValue.Elem().Type().Name() + return strings.ToLower(typeName) +} diff --git a/utils/toml/unmarshal.go b/utils/toml/unmarshal.go new file mode 100644 index 0000000..bc4b6f4 --- /dev/null +++ b/utils/toml/unmarshal.go @@ -0,0 +1,11 @@ +package toml + +import "github.com/pelletier/go-toml/v2" + +func unmarshalContent(data []byte, target any) error { + return toml.Unmarshal(data, target) +} + +func marshalSection(sectionData any) ([]byte, error) { + return toml.Marshal(sectionData) +} |
