aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go36
-rw-r--r--config/constants.go10
-rw-r--r--config/functions.go58
-rw-r--r--config/types.go60
-rw-r--r--enums/mailbox.go8
-rw-r--r--go.mod10
-rw-r--r--go.sum16
-rw-r--r--messages/config.go10
-rw-r--r--messages/logger.go5
-rw-r--r--messages/toml.go5
-rw-r--r--utils/collections/types.go3
-rw-r--r--utils/errors/errors.go14
-rw-r--r--utils/logger/constants.go25
-rw-r--r--utils/logger/functions.go53
-rw-r--r--utils/logger/logger.go83
-rw-r--r--utils/logger/types.go11
-rw-r--r--utils/toml/defaults.go64
-rw-r--r--utils/toml/load.go21
-rw-r--r--utils/toml/parse.go40
-rw-r--r--utils/toml/unmarshal.go11
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"
+)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..f96ee60
--- /dev/null
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a99001e
--- /dev/null
+++ b/go.sum
@@ -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)
+}