diff options
| author | Bobby <[email protected]> | 2026-03-08 03:06:23 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-08 03:06:23 +0530 |
| commit | caf265e7050edefa64ecf7e13828ec9636bce867 (patch) | |
| tree | 6bb8554dbb34695a74c2dca556bf512998cf62ab | |
| parent | cca905d35412f1549400fc3d1aca6dc704d8cae6 (diff) | |
| download | dove-caf265e7050edefa64ecf7e13828ec9636bce867.tar.xz dove-caf265e7050edefa64ecf7e13828ec9636bce867.zip | |
Refactor configuration handling and add mail management features
- Removed dependency on messages package in TOML loading and parsing.
- Introduced new config constants and messages for better clarity and maintainability.
- Implemented mail user and mailbox management with corresponding controllers and views.
- Added new templates for mailboxes, mailbox creation, and user management.
- Enhanced logging and error handling throughout the application.
- Established a structured approach for applying default values in TOML configuration.
- Created new utility functions for SMTP and email handling.
98 files changed, 832 insertions, 1400 deletions
diff --git a/config/config.go b/config/config.go index 7465da7..a6326de 100644 --- a/config/config.go +++ b/config/config.go @@ -1,16 +1,95 @@ package config import ( - "dove/messages" + "os" + "path/filepath" + "strings" + "dove/utils/logger" + "dove/utils/toml" ) +type Http 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 Dns struct { + Host string `toml:"host" default:"127.0.0.1"` + Port int `toml:"port" default:"5053"` + DefaultTTL int `toml:"default_ttl" default:"300"` +} + +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"` + Domain string `toml:"domain" default:"localhost"` + MaxMessageSize int `toml:"max_message_size" default:"26214400"` + ReadTimeout int `toml:"read_timeout" default:"30"` + WriteTimeout int `toml:"write_timeout" default:"30"` + 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 Storage struct { + Host string `toml:"host" default:"0.0.0.0"` + Port int `toml:"port" default:"5900"` + DataDir string `toml:"data_dir" default:"storage"` + MaxObjectSize int64 `toml:"max_object_size" default:"536870912"` +} + +type PortAssignment struct { + Service string + Host string + Port int +} + var ( - IMAP imap - Mailbox mailbox - POP3 pop3 - Server server - SMTP smtp + HTTP Http + DNS Dns + SMTP Smtp + IMAP Imap + POP3 Pop3 + S3 Storage ) var ( @@ -27,24 +106,90 @@ func init() { configFilePath := resolveConfigFilePath(DevMode, osConfigDirectory) if createError := ensureConfigFileExists(configFilePath); createError != nil { - logger.Fatalf(LOG_PREFIX, messages.ConfigCreateFailed, createError) + logger.Fatalf(LogPrefix, ConfigCreateFailed, createError) } if loadError := loadConfigFile(configFilePath); loadError != nil { - logger.Fatalf(LOG_PREFIX, messages.ConfigFileLoadFailed, loadError) + logger.Fatalf(LogPrefix, ConfigFileLoadFailed, loadError) } - for _, section := range []any{&Server, &SMTP, &IMAP, &POP3, &Mailbox} { + for _, section := range []any{&HTTP, &DNS, &SMTP, &IMAP, &POP3, &S3} { if parseError := parseSection(section); parseError != nil { - logger.Fatalf(LOG_PREFIX, messages.ConfigSectionParseFailed, parseError) + logger.Fatalf(LogPrefix, ConfigSectionFailed, parseError) } } AuthEnabled = isAuthEnabled() if portError := ValidatePorts(); portError != nil { - logger.Fatalf(LOG_PREFIX, messages.ConfigPortValidFailed, portError) + logger.Fatalf(LogPrefix, ConfigPortValidFailed, portError) } - logger.Successf(LOG_PREFIX, messages.ConfigLoaded) + logger.Successf(LogPrefix, ConfigLoaded) +} + +func isDevelopmentMode() bool { + executablePath, pathError := os.Executable() + if pathError != nil { + return false + } + + return strings.Contains(executablePath, GoBuildPathIndicator) || + strings.Contains(executablePath, GoBuildAltIndicator) +} + +func resolveOSConfigDirectory() string { + configDirectory, directoryError := os.UserConfigDir() + if directoryError != nil { + return CurrentDirectory + } + + return filepath.Join(configDirectory, ApplicationDirectory) +} + +func resolveConfigFilePath(developmentMode bool, osConfigDirectory string) string { + switch developmentMode { + case true: + return filepath.Join(CurrentDirectory, ConfigFileName) + default: + return filepath.Join(osConfigDirectory, ConfigFileName) + } +} + +func resolveDataDirectory(developmentMode bool, osConfigDirectory string) string { + switch developmentMode { + case true: + return CurrentDirectory + default: + return osConfigDirectory + } +} + +func ensureConfigFileExists(configFilePath string) error { + if _, statError := os.Stat(configFilePath); statError == nil { + return nil + } + + configDirectory := filepath.Dir(configFilePath) + if mkdirError := os.MkdirAll(configDirectory, ConfigDirectoryPermissions); mkdirError != nil { + return mkdirError + } + + return os.WriteFile(configFilePath, defaultConfigContent, ConfigFilePermissions) +} + +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) +} + +func isAuthEnabled() bool { + return HTTP.Username != "" && HTTP.Password != "" } diff --git a/config/constants.go b/config/constants.go deleted file mode 100644 index dae1811..0000000 --- a/config/constants.go +++ /dev/null @@ -1,12 +0,0 @@ -package config - -const ( - APPLICATION_DIRECTORY = "dove" - CONFIG_DIRECTORY_PERMISSIONS = 0755 - CONFIG_FILE_NAME = "config.toml" - CONFIG_FILE_PERMISSIONS = 0644 - CURRENT_DIRECTORY = "." - GO_BUILD_ALT_INDICATOR = "go_build" - GO_BUILD_PATH_INDICATOR = "go-build" - LOG_PREFIX = "Config" -) diff --git a/config/defaults.go b/config/defaults.go new file mode 100644 index 0000000..1962820 --- /dev/null +++ b/config/defaults.go @@ -0,0 +1,12 @@ +package config + +const ( + ApplicationDirectory = "dove" + ConfigDirectoryPermissions = 0755 + ConfigFileName = "config.toml" + ConfigFilePermissions = 0644 + CurrentDirectory = "." + GoBuildAltIndicator = "go_build" + GoBuildPathIndicator = "go-build" + LogPrefix = "Config" +) diff --git a/config/functions.go b/config/functions.go deleted file mode 100644 index c6ce601..0000000 --- a/config/functions.go +++ /dev/null @@ -1,75 +0,0 @@ -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 ensureConfigFileExists(configFilePath string) error { - if _, statError := os.Stat(configFilePath); statError == nil { - return nil - } - - configDirectory := filepath.Dir(configFilePath) - if mkdirError := os.MkdirAll(configDirectory, CONFIG_DIRECTORY_PERMISSIONS); mkdirError != nil { - return mkdirError - } - - return os.WriteFile(configFilePath, defaultConfigContent, CONFIG_FILE_PERMISSIONS) -} - -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) -} - -func isAuthEnabled() bool { - return Server.Username != "" && Server.Password != "" -} diff --git a/config/messages.go b/config/messages.go new file mode 100644 index 0000000..6cd3d9b --- /dev/null +++ b/config/messages.go @@ -0,0 +1,10 @@ +package config + +const ( + ConfigCreateFailed = "Failed to create config file: %v" + ConfigFileLoadFailed = "Failed to load config: %v" + ConfigLoaded = "Configuration loaded successfully." + ConfigPortCollision = "Port collision: %s and %s both configured on %s." + ConfigPortValidFailed = "Port validation failed: %v" + ConfigSectionFailed = "Failed to parse config section: %v" +) diff --git a/config/types.go b/config/types.go deleted file mode 100644 index 33d37f7..0000000 --- a/config/types.go +++ /dev/null @@ -1,68 +0,0 @@ -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"` - Domain string `toml:"domain" default:"localhost"` - MaxMessageSize int `toml:"max_message_size" default:"26214400"` - ReadTimeout int `toml:"read_timeout" default:"30"` - WriteTimeout int `toml:"write_timeout" default:"30"` - 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"` -} - -type portAssignment struct { - service string - host string - port int -} diff --git a/config/validation.go b/config/validation.go index 46b85ac..bbdcd9b 100644 --- a/config/validation.go +++ b/config/validation.go @@ -3,7 +3,6 @@ package config import ( "fmt" - "dove/messages" "dove/utils/errors" ) @@ -12,74 +11,44 @@ func ValidatePorts() error { occupiedPorts := make(map[string]string) for _, assignment := range portAssignments { - portKey := fmt.Sprintf("%s:%d", assignment.host, assignment.port) + portKey := fmt.Sprintf("%s:%d", assignment.Host, assignment.Port) if existingService, occupied := occupiedPorts[portKey]; occupied { - return errors.Error(messages.ConfigPortCollision, assignment.service, existingService, portKey) + return errors.Error(ConfigPortCollision, assignment.Service, existingService, portKey) } - occupiedPorts[portKey] = assignment.service + occupiedPorts[portKey] = assignment.Service } return nil } -func collectPortAssignments() []portAssignment { - assignments := []portAssignment{ - { - service: "HTTP", - host: Server.Host, - port: Server.Port, - }, - { - service: "SMTP", - host: SMTP.Host, - port: SMTP.Port, - }, - { - service: "IMAP", - host: IMAP.Host, - port: IMAP.Port, - }, - { - service: "POP3", - host: POP3.Host, - port: POP3.Port, - }, +func collectPortAssignments() []PortAssignment { + assignments := []PortAssignment{ + {Service: "HTTP", Host: HTTP.Host, Port: HTTP.Port}, + {Service: "DNS", Host: DNS.Host, Port: DNS.Port}, + {Service: "SMTP", Host: SMTP.Host, Port: SMTP.Port}, + {Service: "IMAP", Host: IMAP.Host, Port: IMAP.Port}, + {Service: "POP3", Host: POP3.Host, Port: POP3.Port}, + {Service: "S3", Host: S3.Host, Port: S3.Port}, } if SMTP.TLSEnabled { assignments = append(assignments, - portAssignment{ - service: "SMTPS", - host: SMTP.Host, - port: SMTP.SMTPSPort, - }, - portAssignment{ - service: "SMTP STARTTLS", - host: SMTP.Host, - port: SMTP.StartTLSPort, - }, + PortAssignment{Service: "SMTPS", Host: SMTP.Host, Port: SMTP.SMTPSPort}, + PortAssignment{Service: "SMTP STARTTLS", Host: SMTP.Host, Port: SMTP.StartTLSPort}, ) } if IMAP.TLSEnabled { assignments = append(assignments, - portAssignment{ - service: "IMAPS", - host: IMAP.Host, - port: IMAP.IMAPSPort, - }, + PortAssignment{Service: "IMAPS", Host: IMAP.Host, Port: IMAP.IMAPSPort}, ) } if POP3.TLSEnabled { assignments = append(assignments, - portAssignment{ - service: "POP3S", - host: POP3.Host, - port: POP3.POP3SPort, - }, + PortAssignment{Service: "POP3S", Host: POP3.Host, Port: POP3.POP3SPort}, ) } diff --git a/controllers/auth.go b/controllers/auth/auth.go index 444026a..8140ddd 100644 --- a/controllers/auth.go +++ b/controllers/auth/auth.go @@ -1,8 +1,7 @@ -package controllers +package auth import ( - "dove/services" - "dove/types" + authService "dove/services/auth" "dove/utils/meta" "dove/utils/shortcuts" @@ -10,12 +9,12 @@ import ( ) func Login(context *fiber.Ctx) error { - body, parseError := meta.Body[types.LoginRequest](context) + body, parseError := meta.Body[authService.LoginRequest](context) if parseError != nil { - return shortcuts.BadRequest(context, parseError) + return shortcuts.BadRequestError(context, parseError) } - _, serviceError := services.Authenticate(context, body) + _, serviceError := authService.Authenticate(context, body) if serviceError != nil { return shortcuts.HandleError(context, serviceError) } @@ -24,7 +23,7 @@ func Login(context *fiber.Ctx) error { } func Logout(context *fiber.Ctx) error { - _, serviceError := services.Deauthenticate(context) + _, serviceError := authService.Deauthenticate(context) if serviceError != nil { return shortcuts.HandleError(context, serviceError) } diff --git a/controllers/domain.go b/controllers/domain/domain.go index 90a5727..1260e3d 100644 --- a/controllers/domain.go +++ b/controllers/domain/domain.go @@ -1,4 +1,4 @@ -package controllers +package domain import ( domainService "dove/services/domain" @@ -11,7 +11,7 @@ import ( func CreateDomain(context *fiber.Ctx) error { body, parseError := meta.Body[domainService.CreateDomainRequest](context) if parseError != nil { - return shortcuts.BadRequest(context, parseError) + return shortcuts.BadRequestError(context, parseError) } serviceError := domainService.CreateDomain(body) @@ -25,7 +25,7 @@ func CreateDomain(context *fiber.Ctx) error { func CreateTLD(context *fiber.Ctx) error { body, parseError := meta.Body[domainService.CreateTLDRequest](context) if parseError != nil { - return shortcuts.BadRequest(context, parseError) + return shortcuts.BadRequestError(context, parseError) } serviceError := domainService.CreateTLD(body) diff --git a/controllers/mail/mail.go b/controllers/mail/mail.go new file mode 100644 index 0000000..976076b --- /dev/null +++ b/controllers/mail/mail.go @@ -0,0 +1,37 @@ +package mail + +import ( + mailService "dove/services/mail" + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func CreateUser(context *fiber.Ctx) error { + body, parseError := meta.Body[mailService.CreateUserRequest](context) + if parseError != nil { + return shortcuts.BadRequestError(context, parseError) + } + + serviceError := mailService.CreateUser(body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "dashboard.users") +} + +func CreateMailbox(context *fiber.Ctx) error { + body, parseError := meta.Body[mailService.CreateMailboxRequest](context) + if parseError != nil { + return shortcuts.BadRequestError(context, parseError) + } + + serviceError := mailService.CreateMailbox(body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "dashboard.mailboxes") +}
\ No newline at end of file diff --git a/controllers/mailbox.go b/controllers/mailbox.go deleted file mode 100644 index 0878bd3..0000000 --- a/controllers/mailbox.go +++ /dev/null @@ -1,24 +0,0 @@ -package controllers - -import ( - "dove/services" - "dove/types" - "dove/utils/meta" - "dove/utils/shortcuts" - - "github.com/gofiber/fiber/v2" -) - -func CreateMailbox(context *fiber.Ctx) error { - body, parseError := meta.Body[types.CreateMailboxRequest](context) - if parseError != nil { - return shortcuts.BadRequest(context, parseError) - } - - serviceError := services.CreateMailbox(body) - if serviceError != nil { - return shortcuts.HandleError(context, serviceError) - } - - return shortcuts.Redirect(context, "dashboard.mailboxes") -}
\ No newline at end of file diff --git a/controllers/user.go b/controllers/user.go deleted file mode 100644 index ea2008b..0000000 --- a/controllers/user.go +++ /dev/null @@ -1,24 +0,0 @@ -package controllers - -import ( - "dove/services" - "dove/types" - "dove/utils/meta" - "dove/utils/shortcuts" - - "github.com/gofiber/fiber/v2" -) - -func CreateUser(context *fiber.Ctx) error { - body, parseError := meta.Body[types.CreateUserRequest](context) - if parseError != nil { - return shortcuts.BadRequest(context, parseError) - } - - serviceError := services.CreateUser(body) - if serviceError != nil { - return shortcuts.HandleError(context, serviceError) - } - - return shortcuts.Redirect(context, "dashboard.users") -}
\ No newline at end of file diff --git a/database/migration.go b/database/migration.go index e18f2d6..efd3aba 100644 --- a/database/migration.go +++ b/database/migration.go @@ -1,8 +1,8 @@ package database import ( - "dove/models" "dove/models/domain" + "dove/models/mail" "dove/utils/logger" ) @@ -10,15 +10,15 @@ func migrate() { migrationError := DB.AutoMigrate( &domain.TLD{}, &domain.Domain{}, - &models.User{}, - &models.Mailbox{}, - &models.Alias{}, - &models.Email{}, - &models.Tag{}, - &models.Attachment{}, + &mail.User{}, + &mail.Mailbox{}, + &mail.Alias{}, + &mail.Email{}, + &mail.Tag{}, + &mail.Attachment{}, ) if migrationError != nil { logger.Fatalf(LogPrefix, MigrationFailed, migrationError) } -} +}
\ No newline at end of file diff --git a/database/functions.go b/database/resolve.go index 09d6ae4..db90dc5 100644 --- a/database/functions.go +++ b/database/resolve.go @@ -13,10 +13,10 @@ func resolveDatabasePath() string { } func resolveGORMLogLevel() logger.Interface { - switch config.Server.Debug { + switch config.HTTP.Debug { case true: return logger.Default.LogMode(logger.Info) default: return logger.Default.LogMode(logger.Silent) } -} +}
\ No newline at end of file diff --git a/dove/main.go b/dove/main.go index 4de7ef8..d03fd0a 100644 --- a/dove/main.go +++ b/dove/main.go @@ -7,34 +7,30 @@ import ( "syscall" "dove/config" - "dove/messages" "dove/middleware" "dove/router" "dove/tags" "dove/utils/logger" "dove/utils/smtp" + _ "dove/repositories/init" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/template/django/v3" - "github.com/spf13/cobra" ) -const LOG_PREFIX = "Server" +const LogPrefix = "Server" -var rootCommand = &cobra.Command{ - Use: "dove", - Short: "Local SMTP email testing tool.", - RunE: serve, -} +const ( + ServerStarting = "Server started on %s." + ServerListenFailed = "Failed to start server: %v" + ServerShuttingDown = "Shutting down gracefully..." + ServerShutdownFailed = "Error during server shutdown: %v" + ServerShutdownComplete = "Shutdown complete." +) func main() { - if executeError := rootCommand.Execute(); executeError != nil { - logger.Fatalf(LOG_PREFIX, "%v", executeError) - } -} - -func serve(command *cobra.Command, arguments []string) error { tags.Initialize() engine := django.New("./templates", ".django") @@ -57,23 +53,22 @@ func serve(command *cobra.Command, arguments []string) error { signal.Notify(shutdownSignal, syscall.SIGINT, syscall.SIGTERM) go func() { - address := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port) - logger.Successf(LOG_PREFIX, messages.ServerStarting, address) + address := fmt.Sprintf("%s:%d", config.HTTP.Host, config.HTTP.Port) + logger.Successf(LogPrefix, ServerStarting, address) if listenError := application.Listen(address); listenError != nil { - logger.Fatalf(LOG_PREFIX, messages.ServerListenFailed, listenError) + logger.Fatalf(LogPrefix, ServerListenFailed, listenError) } }() <-shutdownSignal - logger.Infof(LOG_PREFIX, messages.ServerShuttingDown) + logger.Infof(LogPrefix, ServerShuttingDown) smtp.Shutdown() if shutdownError := application.Shutdown(); shutdownError != nil { - logger.Errorf(LOG_PREFIX, messages.ServerShutdownFailed, shutdownError) + logger.Errorf(LogPrefix, ServerShutdownFailed, shutdownError) } - logger.Successf(LOG_PREFIX, messages.ServerShutdownComplete) - return nil -} + logger.Successf(LogPrefix, ServerShutdownComplete) +}
\ No newline at end of file diff --git a/example.config.toml b/example.config.toml index be3cdaf..962bea1 100644 --- a/example.config.toml +++ b/example.config.toml @@ -1,34 +1,66 @@ # ============================================================================= -# Dove - Local SMTP Email Testing Tool +# Dove — Local Infrastructure Service for Peaceful Development Experience # ============================================================================= +# +# This is the default configuration file for Dove. It controls how each +# service binds to the network and behaves at startup. Settings that can be +# managed at runtime (queue policies, health check intervals, cron schedules, +# KV limits, etc.) are configured through the dashboard and stored in the +# database — they do not appear here. +# # Copy this file to config.toml and adjust values as needed. # All values shown below are defaults and can be omitted if unchanged. +# Commented-out values are optional and disabled by default. -# ----------------------------------------------------------------------------- +# ============================================================================= # HTTP Server -# ----------------------------------------------------------------------------- -# The web dashboard for viewing and managing captured emails. +# ============================================================================= +# The web dashboard and REST API for managing your local infrastructure. +# This is the primary interface for interacting with Dove. -[server] +[http] # Network interface to bind the HTTP server to. # Use "0.0.0.0" to listen on all interfaces, or "127.0.0.1" for local only. host = "0.0.0.0" -# Port for the web dashboard. +# Port for the web dashboard and API. port = 8080 -# Enable verbose debug logging for HTTP requests. +# Enable verbose debug logging for HTTP requests and responses. debug = false # Optional credentials to protect the web dashboard. -# Leave commented out to disable dashboard authentication. +# When set, users must authenticate before accessing the dashboard. +# Leave commented out to allow unrestricted access. # username = "" # password = "" -# ----------------------------------------------------------------------------- +# ============================================================================= +# DNS Server +# ============================================================================= +# Resolves custom TLDs (.dove, .local, .test, .nest) and all registered +# domains. A records with port mappings act as a built-in reverse proxy, +# routing domain requests to local services automatically. + +[dns] +# Network interface to bind the DNS server to. +# Typically bound to localhost since it serves local resolution only. +host = "127.0.0.1" + +# Port for DNS queries over UDP. +# The standard DNS port is 53, but a non-privileged port is used by default +# to avoid requiring root permissions. +port = 5053 + +# Default time-to-live in seconds for DNS records when not explicitly set. +default_ttl = 300 + +# ============================================================================= # SMTP Server -# ----------------------------------------------------------------------------- -# Receives incoming emails from mail clients and applications. +# ============================================================================= +# Receives incoming emails for mailboxes registered under your domains. +# Emails sent to non-existent mailboxes on existing domains will bounce. +# Emails sent to non-existent domains are silently dropped. [smtp] # Network interface to bind the SMTP server to. @@ -37,17 +69,19 @@ host = "0.0.0.0" # Port for plain SMTP connections. port = 5025 -# Port for implicit TLS (SMTPS) connections. Requires tls_enabled = true. +# Port for implicit TLS (SMTPS) connections. +# Only active when tls_enabled is set to true. smtps_port = 5465 -# Port for STARTTLS connections. Requires tls_enabled = true. +# Port for STARTTLS upgrade connections. +# Only active when tls_enabled is set to true. starttls_port = 5587 # The domain name announced in SMTP EHLO/HELO greetings. # This identifies the mail server to connecting clients. domain = "localhost" -# Maximum allowed email size in bytes (default: 25 MB). +# Maximum allowed email size in bytes. Default is 25 MB. max_message_size = 26214400 # Timeout in seconds for reading data from SMTP clients. @@ -57,21 +91,25 @@ read_timeout = 30 write_timeout = 30 # Require SMTP authentication before accepting messages. +# When enabled, clients must authenticate with the credentials below. auth_required = false -# Credentials for SMTP authentication. Only used when auth_required = true. +# Credentials for SMTP authentication. +# Only used when auth_required is set to true. # username = "" # password = "" -# Enable TLS support for SMTPS and STARTTLS listeners. +# Enable TLS support for encrypted SMTPS and STARTTLS listeners. +# Requires valid certificate and key paths below. tls_enabled = false -# Paths to TLS certificate and private key files. Required when tls_enabled = true. +# Paths to the TLS certificate and private key files. +# Required when tls_enabled is set to true. # tls_cert = "" # tls_key = "" -# Relay captured emails to an upstream SMTP server. -# Useful for forwarding test emails to a real mail server. +# Relay configuration for forwarding emails to an upstream SMTP server. +# Useful for testing email delivery through a real mail provider. relay_enabled = false # relay_host = "" relay_port = 587 @@ -79,10 +117,11 @@ relay_port = 587 # relay_password = "" relay_starttls = true -# ----------------------------------------------------------------------------- +# ============================================================================= # IMAP Server -# ----------------------------------------------------------------------------- -# Allows mail clients to retrieve captured emails via the IMAP protocol. +# ============================================================================= +# Provides IMAP access for external mail clients (Thunderbird, Apple Mail, +# etc.) to retrieve emails stored in Dove mailboxes. [imap] # Network interface to bind the IMAP server to. @@ -91,27 +130,32 @@ host = "0.0.0.0" # Port for plain IMAP connections. port = 5143 -# Port for implicit TLS (IMAPS) connections. Requires tls_enabled = true. +# Port for implicit TLS (IMAPS) connections. +# Only active when tls_enabled is set to true. imaps_port = 5993 -# Require IMAP authentication before granting access. +# Require IMAP authentication before granting mailbox access. auth_required = false -# Credentials for IMAP authentication. Only used when auth_required = true. +# Credentials for IMAP authentication. +# Only used when auth_required is set to true. # username = "" # password = "" -# Enable TLS support for the IMAPS listener. +# Enable TLS support for encrypted IMAPS connections. +# Requires valid certificate and key paths below. tls_enabled = false -# Paths to TLS certificate and private key files. Required when tls_enabled = true. +# Paths to the TLS certificate and private key files. +# Required when tls_enabled is set to true. # tls_cert = "" # tls_key = "" -# ----------------------------------------------------------------------------- +# ============================================================================= # POP3 Server -# ----------------------------------------------------------------------------- -# Allows mail clients to retrieve captured emails via the POP3 protocol. +# ============================================================================= +# Provides POP3 access for external mail clients to download and remove +# emails from Dove mailboxes. [pop3] # Network interface to bind the POP3 server to. @@ -120,30 +164,43 @@ host = "0.0.0.0" # Port for plain POP3 connections. port = 5110 -# Port for implicit TLS (POP3S) connections. Requires tls_enabled = true. +# Port for implicit TLS (POP3S) connections. +# Only active when tls_enabled is set to true. pop3s_port = 5995 -# Require POP3 authentication before granting access. +# Require POP3 authentication before granting mailbox access. auth_required = false -# Credentials for POP3 authentication. Only used when auth_required = true. +# Credentials for POP3 authentication. +# Only used when auth_required is set to true. # username = "" # password = "" -# Enable TLS support for the POP3S listener. +# Enable TLS support for encrypted POP3S connections. +# Requires valid certificate and key paths below. tls_enabled = false -# Paths to TLS certificate and private key files. Required when tls_enabled = true. +# Paths to the TLS certificate and private key files. +# Required when tls_enabled is set to true. # tls_cert = "" # tls_key = "" -# ----------------------------------------------------------------------------- -# Mailbox -# ----------------------------------------------------------------------------- -# Controls how incoming emails are routed to mailboxes. +# ============================================================================= +# S3-Compatible Object Storage +# ============================================================================= +# Filesystem-backed object storage with full S3 API compatibility. +# Works with aws-sdk, boto3, minio-go, and any S3-compatible client. + +[storage] +# Network interface to bind the S3 API server to. +host = "0.0.0.0" + +# Port for S3 API requests. +port = 5900 + +# Directory where object data is stored on disk. +# Relative paths are resolved from Dove's data directory. +data_dir = "storage" -[mailbox] -# Mailbox routing mode: -# "registered" - Only accept emails for registered mailbox addresses. -# "open" - Automatically create mailboxes for any recipient address. -mode = "registered" +# Maximum allowed object size in bytes. Default is 512 MB. +max_object_size = 536870912 diff --git a/models/alias.go b/models/alias.go deleted file mode 100644 index 5b0dcb3..0000000 --- a/models/alias.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "gorm.io/gorm" - -type Alias struct { - gorm.Model - SourceAddress string `gorm:"uniqueIndex;not null" json:"source_address"` - MailboxID uint `gorm:"not null" json:"mailbox_id"` - Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"` -} diff --git a/models/attachment.go b/models/attachment.go deleted file mode 100644 index cd1a741..0000000 --- a/models/attachment.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import "gorm.io/gorm" - -type Attachment struct { - gorm.Model - EmailID uint `gorm:"not null;index" json:"email_id"` - Email Email `gorm:"foreignKey:EmailID" json:"email"` - Filename string `gorm:"not null" json:"filename"` - ContentType string `gorm:"not null" json:"content_type"` - ContentID string `json:"content_id"` - Size int64 `json:"size"` - IsInline bool `gorm:"default:false" json:"is_inline"` -} diff --git a/models/email.go b/models/email.go deleted file mode 100644 index 12b3ad1..0000000 --- a/models/email.go +++ /dev/null @@ -1,25 +0,0 @@ -package models - -import "gorm.io/gorm" - -type Email struct { - gorm.Model - MailboxID uint `gorm:"not null;index" json:"mailbox_id"` - Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"` - MessageID string `gorm:"index" json:"message_id"` - Filename string `gorm:"uniqueIndex;not null" json:"filename"` - FromAddress string `gorm:"not null" json:"from_address"` - FromName string `json:"from_name"` - ToAddresses string `gorm:"not null" json:"to_addresses"` - CcAddresses string `json:"cc_addresses"` - BccAddresses string `json:"bcc_addresses"` - ReplyToAddress string `json:"reply_to_address"` - ReturnPath string `json:"return_path"` - Subject string `json:"subject"` - Snippet string `json:"snippet"` - Size int64 `json:"size"` - IsRead bool `gorm:"default:false;index" json:"is_read"` - AttachmentCount int `gorm:"default:0" json:"attachment_count"` - InlineCount int `gorm:"default:0" json:"inline_count"` - Tags []Tag `gorm:"many2many:email_tags" json:"tags"` -} diff --git a/models/mailbox.go b/models/mailbox.go deleted file mode 100644 index da4114f..0000000 --- a/models/mailbox.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import "gorm.io/gorm" - -type Mailbox struct { - gorm.Model - Address string `gorm:"uniqueIndex;not null" json:"address"` - UserID uint `gorm:"not null" json:"user_id"` - User User `gorm:"foreignKey:UserID" json:"user"` - Aliases []Alias `gorm:"foreignKey:MailboxID" json:"aliases"` - Emails []Email `gorm:"foreignKey:MailboxID" json:"emails"` -} diff --git a/models/tag.go b/models/tag.go deleted file mode 100644 index 79fa65f..0000000 --- a/models/tag.go +++ /dev/null @@ -1,9 +0,0 @@ -package models - -import "gorm.io/gorm" - -type Tag struct { - gorm.Model - Name string `gorm:"uniqueIndex;not null" json:"name"` - Emails []Email `gorm:"many2many:email_tags" json:"emails"` -} diff --git a/models/user.go b/models/user.go deleted file mode 100644 index 6bb6edf..0000000 --- a/models/user.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "gorm.io/gorm" - -type User struct { - gorm.Model - Username string `gorm:"uniqueIndex;not null" json:"username"` - DisplayName string `json:"display_name"` - Mailboxes []Mailbox `gorm:"foreignKey:UserID" json:"mailboxes"` -} diff --git a/pages/dashboard.go b/pages/dashboard.go deleted file mode 100644 index 3fb16fb..0000000 --- a/pages/dashboard.go +++ /dev/null @@ -1,14 +0,0 @@ -package pages - -import ( - "dove/services" - "dove/utils/meta" - "dove/utils/shortcuts" - - "github.com/gofiber/fiber/v2" -) - -func Dashboard(context *fiber.Ctx) error { - meta.SetPageTitle(context, "Overview") - return shortcuts.Render(context, "dashboard/overview", services.Overview()) -} diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go new file mode 100644 index 0000000..319f884 --- /dev/null +++ b/pages/dashboard/dashboard.go @@ -0,0 +1,13 @@ +package dashboard + +import ( + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Overview(context *fiber.Ctx) error { + meta.SetPageTitle(context, "Overview") + return shortcuts.Render(context, "dashboard/overview", nil) +} diff --git a/pages/domain.go b/pages/domain/domain.go index d46af9c..3d01df0 100644 --- a/pages/domain.go +++ b/pages/domain/domain.go @@ -1,4 +1,4 @@ -package pages +package domain import ( domainService "dove/services/domain" @@ -21,4 +21,4 @@ func NewDomain(context *fiber.Ctx) error { func NewTLD(context *fiber.Ctx) error { meta.SetPageTitle(context, "New TLD") return shortcuts.Render(context, "domains/newtld", nil) -}
\ No newline at end of file +} diff --git a/pages/home.go b/pages/home/home.go index 06f836e..6107da1 100644 --- a/pages/home.go +++ b/pages/home/home.go @@ -1,4 +1,4 @@ -package pages +package home import ( "dove/config" diff --git a/pages/mailbox.go b/pages/mail/mailbox.go index a444a73..81f525b 100644 --- a/pages/mailbox.go +++ b/pages/mail/mailbox.go @@ -1,7 +1,7 @@ -package pages +package mail import ( - "dove/types" + mailService "dove/services/mail" "dove/utils/meta" "dove/utils/shortcuts" @@ -9,9 +9,9 @@ import ( ) func Mailbox(context *fiber.Ctx) error { - address := meta.Request(context).Param("address").Required() + address := meta.Request(context).Param("address") meta.SetPageTitle(context, address) - return shortcuts.Render(context, "dashboard/mailbox", types.Mailbox{ + return shortcuts.Render(context, "mail/mailbox", mailService.MailboxView{ Address: address, }) } diff --git a/pages/mailboxes.go b/pages/mail/mailboxes.go index 611c2cb..36d9041 100644 --- a/pages/mailboxes.go +++ b/pages/mail/mailboxes.go @@ -1,7 +1,7 @@ -package pages +package mail import ( - "dove/services" + mailService "dove/services/mail" "dove/utils/meta" "dove/utils/shortcuts" @@ -10,7 +10,7 @@ import ( func NewMailbox(context *fiber.Ctx) error { meta.SetPageTitle(context, "New Mailbox") - return shortcuts.Render(context, "dashboard/newmailbox", services.MailboxFormData()) + return shortcuts.Render(context, "mail/newmailbox", mailService.MailboxFormData()) } func Mailboxes(context *fiber.Ctx) error { @@ -20,5 +20,5 @@ func Mailboxes(context *fiber.Ctx) error { sorting := meta.Sort(context, []string{"address", "created_at"}, "created_at") search := context.Query("search") - return shortcuts.Render(context, "dashboard/mailboxes", services.ListMailboxes(pagination, sorting, search)) + return shortcuts.Render(context, "mail/mailboxes", mailService.ListMailboxes(pagination, sorting, search)) } diff --git a/pages/users.go b/pages/mail/users.go index 1fc6fcc..8700c80 100644 --- a/pages/users.go +++ b/pages/mail/users.go @@ -1,7 +1,7 @@ -package pages +package mail import ( - "dove/services" + mailService "dove/services/mail" "dove/utils/meta" "dove/utils/shortcuts" @@ -10,7 +10,7 @@ import ( func NewUser(context *fiber.Ctx) error { meta.SetPageTitle(context, "New User") - return shortcuts.Render(context, "dashboard/newuser", nil) + return shortcuts.Render(context, "mail/newuser", nil) } func Users(context *fiber.Ctx) error { @@ -20,5 +20,5 @@ func Users(context *fiber.Ctx) error { sorting := meta.Sort(context, []string{"username", "display_name", "created_at"}, "created_at") search := context.Query("search") - return shortcuts.Render(context, "dashboard/users", services.ListUsers(pagination, sorting, search)) + return shortcuts.Render(context, "mail/users", mailService.ListUsers(pagination, sorting, search)) } diff --git a/repositories/alias.go b/repositories/alias.go deleted file mode 100644 index ee038b2..0000000 --- a/repositories/alias.go +++ /dev/null @@ -1,18 +0,0 @@ -package repositories - -import ( - "dove/database" - "dove/models" - - "gorm.io/gorm" -) - -func FindAliasByAddress(address string) *models.Alias { - var alias models.Alias - result := database.DB.Preload("Mailbox").Where("source_address = ?", address).First(&alias) - if result.Error == gorm.ErrRecordNotFound { - return nil - } - - return &alias -} diff --git a/repositories/constants.go b/repositories/constants.go deleted file mode 100644 index 29ce495..0000000 --- a/repositories/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package repositories - -const ( - LOG_PREFIX = "Repositories" -) diff --git a/repositories/email.go b/repositories/email.go deleted file mode 100644 index 26ce620..0000000 --- a/repositories/email.go +++ /dev/null @@ -1,50 +0,0 @@ -package repositories - -import ( - "dove/database" - "dove/models" - "dove/utils/meta" -) - -func CreateEmail(email *models.Email) error { - return database.DB.Create(email).Error -} - -func CreateAttachment(attachment *models.Attachment) error { - return database.DB.Create(attachment).Error -} - -func ListEmails(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.Email, int64) { - var emails []models.Email - var total int64 - - query := database.DB.Model(&models.Email{}) - - if search != "" { - like := "%" + search + "%" - query = query.Where("from_address LIKE ? OR to_addresses LIKE ? OR subject LIKE ?", like, like, like) - } - - query.Count(&total) - pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails) - - return emails, total -} - -func CountEmails() int64 { - var count int64 - database.DB.Model(&models.Email{}).Count(&count) - return count -} - -func ListEmailsByMailbox(mailboxID uint, pagination meta.Pagination, sorting meta.Sorting) ([]models.Email, int64) { - var emails []models.Email - var total int64 - - query := database.DB.Model(&models.Email{}).Where("mailbox_id = ?", mailboxID) - - query.Count(&total) - pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails) - - return emails, total -} diff --git a/repositories/mailbox.go b/repositories/mailbox.go deleted file mode 100644 index 56fb7b1..0000000 --- a/repositories/mailbox.go +++ /dev/null @@ -1,46 +0,0 @@ -package repositories - -import ( - "dove/database" - "dove/models" - "dove/utils/meta" - - "gorm.io/gorm" -) - -func FindMailboxByAddress(address string) *models.Mailbox { - var mailbox models.Mailbox - result := database.DB.Where("address = ?", address).First(&mailbox) - if result.Error == gorm.ErrRecordNotFound { - return nil - } - - return &mailbox -} - -func CreateMailbox(mailbox *models.Mailbox) error { - return database.DB.Create(mailbox).Error -} - -func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.Mailbox, int64) { - var mailboxes []models.Mailbox - var total int64 - - query := database.DB.Model(&models.Mailbox{}) - - if search != "" { - like := "%" + search + "%" - query = query.Where("address LIKE ?", like) - } - - query.Count(&total) - pagination.Apply(sorting.Apply(query)).Preload("User").Find(&mailboxes) - - return mailboxes, total -} - -func CountMailboxes() int64 { - var count int64 - database.DB.Model(&models.Mailbox{}).Count(&count) - return count -} diff --git a/repositories/user.go b/repositories/user.go deleted file mode 100644 index 7a3e5e6..0000000 --- a/repositories/user.go +++ /dev/null @@ -1,62 +0,0 @@ -package repositories - -import ( - "dove/database" - "dove/models" - "dove/utils/meta" - - "gorm.io/gorm" -) - -func FindUserByID(userID uint) *models.User { - var user models.User - result := database.DB.First(&user, userID) - if result.Error == gorm.ErrRecordNotFound { - return nil - } - - return &user -} - -func FindUserByUsername(username string) *models.User { - var user models.User - result := database.DB.Where("username = ?", username).First(&user) - if result.Error == gorm.ErrRecordNotFound { - return nil - } - - return &user -} - -func CreateUser(user *models.User) error { - return database.DB.Create(user).Error -} - -func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.User, int64) { - var users []models.User - var total int64 - - query := database.DB.Model(&models.User{}) - - if search != "" { - like := "%" + search + "%" - query = query.Where("username LIKE ? OR display_name LIKE ?", like, like) - } - - query.Count(&total) - pagination.Apply(sorting.Apply(query)).Preload("Mailboxes").Find(&users) - - return users, total -} - -func AllUsers() []models.User { - var users []models.User - database.DB.Order("username ASC").Find(&users) - return users -} - -func CountUsers() int64 { - var count int64 - database.DB.Model(&models.User{}).Count(&count) - return count -} diff --git a/router/auth.go b/router/auth.go index a0261f1..f4daa6a 100644 --- a/router/auth.go +++ b/router/auth.go @@ -1,13 +1,13 @@ package router import ( - "dove/controllers" + authController "dove/controllers/auth" "dove/utils/urls" ) func init() { urls.SetNamespace("auth") - urls.Path(urls.Post, "/login", controllers.Login, "login") - urls.Path(urls.Get, "/logout", controllers.Logout, "logout") + urls.Path(urls.Post, "/login", authController.Login, "login") + urls.Path(urls.Get, "/logout", authController.Logout, "logout") } diff --git a/router/base.go b/router/base.go index c70c685..1330d3a 100644 --- a/router/base.go +++ b/router/base.go @@ -1,12 +1,12 @@ package router import ( - "dove/pages" + "dove/pages/home" "dove/utils/urls" ) func init() { urls.SetNamespace("") - urls.Path(urls.Get, "/", pages.Home, "home") + urls.Path(urls.Get, "/", home.Home, "home") } diff --git a/router/dashboard.go b/router/dashboard.go index 8baa4b8..5de1dd1 100644 --- a/router/dashboard.go +++ b/router/dashboard.go @@ -1,8 +1,7 @@ package router import ( - "dove/controllers" - "dove/pages" + "dove/pages/dashboard" "dove/utils/auth" "dove/utils/urls" ) @@ -10,12 +9,5 @@ import ( func init() { urls.SetNamespace("dashboard") - urls.Path(urls.Get, "/", auth.RequireAuthentication(pages.Dashboard), "index") - urls.Path(urls.Get, "/mailboxes", auth.RequireAuthentication(pages.Mailboxes), "mailboxes") - urls.Path(urls.Get, "/mailboxes/new", auth.RequireAuthentication(pages.NewMailbox), "mailboxes.new") - urls.Path(urls.Post, "/mailboxes", auth.RequireAuthentication(controllers.CreateMailbox), "mailboxes.create") - urls.Path(urls.Get, "/mailboxes/:address", auth.RequireAuthentication(pages.Mailbox), "mailbox") - urls.Path(urls.Get, "/users", auth.RequireAuthentication(pages.Users), "users") - urls.Path(urls.Get, "/users/new", auth.RequireAuthentication(pages.NewUser), "users.new") - urls.Path(urls.Post, "/users", auth.RequireAuthentication(controllers.CreateUser), "users.create") + urls.Path(urls.Get, "/", auth.RequireAuthentication(dashboard.Overview), "index") } diff --git a/router/domain.go b/router/domain.go index 00ecf36..5658cf2 100644 --- a/router/domain.go +++ b/router/domain.go @@ -1,8 +1,8 @@ package router import ( - "dove/controllers" - "dove/pages" + domainController "dove/controllers/domain" + domainPage "dove/pages/domain" "dove/utils/auth" "dove/utils/urls" ) @@ -10,10 +10,10 @@ import ( func init() { urls.SetNamespace("domains") - urls.Path(urls.Get, "/domains", auth.RequireAuthentication(pages.Domains), "index") - urls.Path(urls.Get, "/domains/new", auth.RequireAuthentication(pages.NewDomain), "new") - urls.Path(urls.Post, "/domains", auth.RequireAuthentication(controllers.CreateDomain), "create") - urls.Path(urls.Get, "/domains/tlds/new", auth.RequireAuthentication(pages.NewTLD), "tlds.new") - urls.Path(urls.Post, "/domains/tlds", auth.RequireAuthentication(controllers.CreateTLD), "tlds.create") - urls.Path(urls.Delete, "/domains/tlds/:name", auth.RequireAuthentication(controllers.DeleteTLD), "tlds.delete") + urls.Path(urls.Get, "/domains", auth.RequireAuthentication(domainPage.Domains), "index") + urls.Path(urls.Get, "/domains/new", auth.RequireAuthentication(domainPage.NewDomain), "new") + urls.Path(urls.Post, "/domains", auth.RequireAuthentication(domainController.CreateDomain), "create") + urls.Path(urls.Get, "/domains/tlds/new", auth.RequireAuthentication(domainPage.NewTLD), "tlds.new") + urls.Path(urls.Post, "/domains/tlds", auth.RequireAuthentication(domainController.CreateTLD), "tlds.create") + urls.Path(urls.Delete, "/domains/tlds/:name", auth.RequireAuthentication(domainController.DeleteTLD), "tlds.delete") } diff --git a/router/mail.go b/router/mail.go new file mode 100644 index 0000000..f2ce1a7 --- /dev/null +++ b/router/mail.go @@ -0,0 +1,20 @@ +package router + +import ( + mailController "dove/controllers/mail" + mailPage "dove/pages/mail" + "dove/utils/auth" + "dove/utils/urls" +) + +func init() { + urls.SetNamespace("mail") + + urls.Path(urls.Get, "/mailboxes", auth.RequireAuthentication(mailPage.Mailboxes), "mailboxes") + urls.Path(urls.Get, "/mailboxes/new", auth.RequireAuthentication(mailPage.NewMailbox), "mailboxes.new") + urls.Path(urls.Post, "/mailboxes", auth.RequireAuthentication(mailController.CreateMailbox), "mailboxes.create") + urls.Path(urls.Get, "/mailboxes/:address", auth.RequireAuthentication(mailPage.Mailbox), "mailbox") + urls.Path(urls.Get, "/users", auth.RequireAuthentication(mailPage.Users), "users") + urls.Path(urls.Get, "/users/new", auth.RequireAuthentication(mailPage.NewUser), "users.new") + urls.Path(urls.Post, "/users", auth.RequireAuthentication(mailController.CreateUser), "users.create") +} diff --git a/router/router.go b/router/router.go index 68ecad9..248f953 100644 --- a/router/router.go +++ b/router/router.go @@ -20,14 +20,14 @@ func ErrorHandler(context *fiber.Ctx, err error) error { switch statusCode { case fiber.StatusBadRequest: - return shortcuts.BadRequest(context, err) + return shortcuts.BadRequestError(context, err) case fiber.StatusForbidden: - return shortcuts.Forbidden(context, err) + return shortcuts.ForbiddenError(context, err) case fiber.StatusNotFound: - return shortcuts.NotFound(context, err) + return shortcuts.NotFoundError(context, err) case fiber.StatusUnauthorized: - return shortcuts.Unauthorized(context, err) + return shortcuts.UnauthorizedError(context, err) default: return shortcuts.InternalServerError(context, err) } -}
\ No newline at end of file +} diff --git a/services/auth.go b/services/auth.go deleted file mode 100644 index ce62d10..0000000 --- a/services/auth.go +++ /dev/null @@ -1,37 +0,0 @@ -package services - -import ( - "dove/config" - "dove/enums" - "dove/messages" - "dove/types" - "dove/utils/auth" - "dove/utils/shortcuts" - - "github.com/gofiber/fiber/v2" -) - -func Authenticate(context *fiber.Ctx, request types.LoginRequest) (*types.MessageResponse, *types.ServiceError) { - switch request.Username == config.Server.Username && request.Password == config.Server.Password { - case true: - if sessionError := auth.Authenticate(context); sessionError != nil { - return nil, shortcuts.ServiceError(enums.Internal, sessionError.Error()) - } - - return &types.MessageResponse{ - Message: messages.AuthAuthenticated, - }, nil - default: - return nil, shortcuts.ServiceError(enums.Unauthorized, messages.AuthInvalidCredentials) - } -} - -func Deauthenticate(context *fiber.Ctx) (*types.MessageResponse, *types.ServiceError) { - if sessionError := auth.Deauthenticate(context); sessionError != nil { - return nil, shortcuts.ServiceError(enums.Internal, sessionError.Error()) - } - - return &types.MessageResponse{ - Message: messages.AuthLoggedOut, - }, nil -}
\ No newline at end of file diff --git a/services/auth/auth.go b/services/auth/auth.go index 89ef205..b8a0e13 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -18,7 +18,7 @@ type MessageResponse struct { } func Authenticate(context *fiber.Ctx, request LoginRequest) (*MessageResponse, *shortcuts.Error) { - switch request.Username == config.Server.Username && request.Password == config.Server.Password { + switch request.Username == config.HTTP.Username && request.Password == config.HTTP.Password { case true: if sessionError := authUtils.Authenticate(context); sessionError != nil { return nil, shortcuts.ServiceError(shortcuts.Internal, sessionError.Error()) diff --git a/services/constants.go b/services/constants.go deleted file mode 100644 index bba619b..0000000 --- a/services/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package services - -const ( - LOG_PREFIX = "Services" -) diff --git a/services/email.go b/services/email.go deleted file mode 100644 index 36ae5f6..0000000 --- a/services/email.go +++ /dev/null @@ -1,31 +0,0 @@ -package services - -import ( - "dove/messages" - "dove/utils/email" - "dove/utils/logger" -) - -func ProcessEmail(rawMessage []byte, recipientAddresses []string) error { - parsedEmail, parseError := email.Parse(rawMessage) - if parseError != nil { - logger.Errorf(LOG_PREFIX, messages.EmailParseFailed, parseError) - return parseError - } - - mailboxes := ResolveMailboxes(recipientAddresses) - if len(mailboxes) == 0 { - return nil - } - - for _, mailbox := range mailboxes { - if storeError := storeEmailForMailbox(rawMessage, parsedEmail, mailbox); storeError != nil { - logger.Errorf(LOG_PREFIX, messages.EmailStoreFailed, storeError) - return storeError - } - } - - logger.Infof(LOG_PREFIX, messages.EmailProcessed, len(mailboxes)) - - return nil -} diff --git a/services/functions.go b/services/functions.go deleted file mode 100644 index ffba7f4..0000000 --- a/services/functions.go +++ /dev/null @@ -1,137 +0,0 @@ -package services - -import ( - "dove/config" - "dove/enums" - "dove/messages" - "dove/models" - "dove/repositories" - "dove/utils/email" - "dove/utils/logger" - "fmt" - "os" - "path/filepath" - "strings" - "time" -) - -func resolveMailbox(address string) *models.Mailbox { - mailbox := repositories.FindMailboxByAddress(address) - if mailbox != nil { - return mailbox - } - - alias := repositories.FindAliasByAddress(address) - if alias != nil { - return &alias.Mailbox - } - - switch config.Mailbox.Mode { - case enums.Catchall: - return createMailboxForAddress(address) - default: - logger.Warnf(LOG_PREFIX, messages.MailboxNotRegistered, address) - return nil - } -} - -func createMailboxForAddress(address string) *models.Mailbox { - user := repositories.FindUserByUsername(address) - if user == nil { - user = &models.User{ - Username: address, - DisplayName: address, - } - repositories.CreateUser(user) - } - - mailbox := &models.Mailbox{ - Address: address, - UserID: user.ID, - } - - repositories.CreateMailbox(mailbox) - logger.Infof(LOG_PREFIX, messages.MailboxAutoCreated, address) - - return mailbox -} - -func storeEmailForMailbox(rawMessage []byte, parsedEmail *email.ParsedEmail, mailbox models.Mailbox) error { - filename, saveError := saveEmailFile(rawMessage, mailbox.ID) - if saveError != nil { - return saveError - } - - attachmentCount, inlineCount := countAttachments(parsedEmail.Attachments) - - emailRecord := &models.Email{ - MailboxID: mailbox.ID, - MessageID: parsedEmail.MessageID, - Filename: filename, - FromAddress: parsedEmail.FromAddress, - FromName: parsedEmail.FromName, - ToAddresses: strings.Join(parsedEmail.ToAddresses, ", "), - CcAddresses: strings.Join(parsedEmail.CcAddresses, ", "), - BccAddresses: strings.Join(parsedEmail.BccAddresses, ", "), - ReplyToAddress: parsedEmail.ReplyToAddress, - ReturnPath: parsedEmail.ReturnPath, - Subject: parsedEmail.Subject, - Snippet: parsedEmail.Snippet, - Size: parsedEmail.Size, - AttachmentCount: attachmentCount, - InlineCount: inlineCount, - } - - if indexError := repositories.CreateEmail(emailRecord); indexError != nil { - logger.Errorf(LOG_PREFIX, messages.EmailIndexFailed, indexError) - return indexError - } - - for _, parsedAttachment := range parsedEmail.Attachments { - attachmentRecord := &models.Attachment{ - EmailID: emailRecord.ID, - Filename: parsedAttachment.Filename, - ContentType: parsedAttachment.ContentType, - ContentID: parsedAttachment.ContentID, - Size: parsedAttachment.Size, - IsInline: parsedAttachment.IsInline, - } - - repositories.CreateAttachment(attachmentRecord) - } - - return nil -} - -func saveEmailFile(rawMessage []byte, mailboxID uint) (string, error) { - mailboxDirectory := filepath.Join(config.DataDir, "emails", fmt.Sprintf("%d", mailboxID)) - - if directoryError := os.MkdirAll(mailboxDirectory, 0750); directoryError != nil { - return "", directoryError - } - - filename := fmt.Sprintf("%d.eml", time.Now().UnixNano()) - filePath := filepath.Join(mailboxDirectory, filename) - - if writeError := os.WriteFile(filePath, rawMessage, 0640); writeError != nil { - return "", writeError - } - - return filename, nil -} - -func countAttachments(attachments []email.ParsedAttachment) (int, int) { - attachmentCount := 0 - inlineCount := 0 - - for _, attachment := range attachments { - switch attachment.IsInline { - case true: - inlineCount++ - default: - attachmentCount++ - } - } - - return attachmentCount, inlineCount -} diff --git a/services/mail/mailboxes.go b/services/mail/mailboxes.go index b124dc9..89af4c4 100644 --- a/services/mail/mailboxes.go +++ b/services/mail/mailboxes.go @@ -3,8 +3,8 @@ package mail import ( "strings" - "dove/models" - "dove/repositories" + mailModel "dove/models/mail" + mailRepo "dove/repositories/mail" "dove/utils/meta" "dove/utils/shortcuts" ) @@ -15,7 +15,7 @@ type CreateMailboxRequest struct { } type MailboxFormResponse struct { - Users []models.User `json:"users"` + Users []mailModel.User `json:"users"` } type MailboxView struct { @@ -23,13 +23,13 @@ type MailboxView struct { } func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) meta.PaginatedResponse { - mailboxes, total := repositories.ListMailboxes(pagination, sorting, search) + mailboxes, total := mailRepo.ListMailboxes(pagination, sorting, search) return pagination.Response(mailboxes, total) } func MailboxFormData() MailboxFormResponse { return MailboxFormResponse{ - Users: repositories.AllUsers(), + Users: mailRepo.AllUsers(), } } @@ -43,20 +43,20 @@ func CreateMailbox(request CreateMailboxRequest) *shortcuts.Error { return shortcuts.ServiceError(shortcuts.BadRequest, UserRequired) } - if repositories.FindUserByID(request.UserID) == nil { + if mailRepo.FindUserByID(request.UserID) == nil { return shortcuts.ServiceError(shortcuts.Unprocessable, UserNotFound) } - if repositories.FindMailboxByAddress(address) != nil { + if mailRepo.FindMailboxByAddress(address) != nil { return shortcuts.ServiceError(shortcuts.Unprocessable, AlreadyExists) } - mailbox := &models.Mailbox{ + mailbox := &mailModel.Mailbox{ Address: address, UserID: request.UserID, } - if createError := repositories.CreateMailbox(mailbox); createError != nil { + if createError := mailRepo.CreateMailbox(mailbox); createError != nil { return shortcuts.ServiceError(shortcuts.Internal, CreationFailed) } diff --git a/services/mail/users.go b/services/mail/users.go index 9520219..9c776a1 100644 --- a/services/mail/users.go +++ b/services/mail/users.go @@ -3,8 +3,8 @@ package mail import ( "strings" - "dove/models" - "dove/repositories" + mailModel "dove/models/mail" + mailRepo "dove/repositories/mail" "dove/utils/meta" "dove/utils/shortcuts" ) @@ -15,7 +15,7 @@ type CreateUserRequest struct { } func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) meta.PaginatedResponse { - users, total := repositories.ListUsers(pagination, sorting, search) + users, total := mailRepo.ListUsers(pagination, sorting, search) return pagination.Response(users, total) } @@ -31,22 +31,22 @@ func CreateUser(request CreateUserRequest) *shortcuts.Error { return shortcuts.ServiceError(shortcuts.BadRequest, DisplayNameRequired) } - if repositories.FindUserByUsername(username) != nil { + if mailRepo.FindUserByUsername(username) != nil { return shortcuts.ServiceError(shortcuts.Unprocessable, UserAlreadyExists) } - newUser := &models.User{ + newUser := &mailModel.User{ Username: username, DisplayName: displayName, } - if createError := repositories.CreateUser(newUser); createError != nil { + if createError := mailRepo.CreateUser(newUser); createError != nil { return shortcuts.ServiceError(shortcuts.Internal, UserCreationFailed) } return nil } -func AllUsers() []models.User { - return repositories.AllUsers() +func AllUsers() []mailModel.User { + return mailRepo.AllUsers() } diff --git a/services/mailbox.go b/services/mailbox.go deleted file mode 100644 index d1e954d..0000000 --- a/services/mailbox.go +++ /dev/null @@ -1,68 +0,0 @@ -package services - -import ( - "strings" - - "dove/enums" - "dove/messages" - "dove/models" - "dove/repositories" - "dove/types" - "dove/utils/meta" - "dove/utils/shortcuts" -) - -func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse { - mailboxes, total := repositories.ListMailboxes(pagination, sorting, search) - return pagination.Response(mailboxes, total) -} - -func MailboxFormData() types.MailboxFormResponse { - return types.MailboxFormResponse{ - Users: repositories.AllUsers(), - } -} - -func CreateMailbox(request types.CreateMailboxRequest) *types.ServiceError { - address := strings.TrimSpace(request.Address) - - if address == "" { - return shortcuts.ServiceError(enums.BadRequest, messages.MailboxAddressRequired) - } - - if request.UserID == 0 { - return shortcuts.ServiceError(enums.BadRequest, messages.MailboxUserRequired) - } - - if repositories.FindUserByID(request.UserID) == nil { - return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxUserNotFound) - } - - if repositories.FindMailboxByAddress(address) != nil { - return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxAlreadyExists) - } - - mailbox := &models.Mailbox{ - Address: address, - UserID: request.UserID, - } - - if createError := repositories.CreateMailbox(mailbox); createError != nil { - return shortcuts.ServiceError(enums.Internal, messages.MailboxCreationFailed) - } - - return nil -} - -func ResolveMailboxes(recipientAddresses []string) []models.Mailbox { - var resolvedMailboxes []models.Mailbox - - for _, address := range recipientAddresses { - mailbox := resolveMailbox(address) - if mailbox != nil { - resolvedMailboxes = append(resolvedMailboxes, *mailbox) - } - } - - return resolvedMailboxes -} diff --git a/services/overview.go b/services/overview.go deleted file mode 100644 index d0d3f60..0000000 --- a/services/overview.go +++ /dev/null @@ -1,16 +0,0 @@ -package services - -import ( - "dove/config" - "dove/repositories" - "dove/types" - "fmt" -) - -func Overview() types.Overview { - return types.Overview{ - MailboxCount: repositories.CountMailboxes(), - EmailCount: repositories.CountEmails(), - SMTPAddress: fmt.Sprintf(":%d", config.SMTP.Port), - } -} diff --git a/services/user.go b/services/user.go deleted file mode 100644 index b2d43cf..0000000 --- a/services/user.go +++ /dev/null @@ -1,50 +0,0 @@ -package services - -import ( - "strings" - - "dove/enums" - "dove/messages" - "dove/models" - "dove/repositories" - "dove/types" - "dove/utils/meta" - "dove/utils/shortcuts" -) - -func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse { - users, total := repositories.ListUsers(pagination, sorting, search) - return pagination.Response(users, total) -} - -func CreateUser(request types.CreateUserRequest) *types.ServiceError { - username := strings.TrimSpace(request.Username) - displayName := strings.TrimSpace(request.DisplayName) - - if username == "" { - return shortcuts.ServiceError(enums.BadRequest, messages.UserUsernameRequired) - } - - if displayName == "" { - return shortcuts.ServiceError(enums.BadRequest, messages.UserDisplayNameRequired) - } - - if repositories.FindUserByUsername(username) != nil { - return shortcuts.ServiceError(enums.Unprocessable, messages.UserAlreadyExists) - } - - user := &models.User{ - Username: username, - DisplayName: displayName, - } - - if createError := repositories.CreateUser(user); createError != nil { - return shortcuts.ServiceError(enums.Internal, messages.UserCreationFailed) - } - - return nil -} - -func AllUsers() []models.User { - return repositories.AllUsers() -} diff --git a/session/constants.go b/session/constants.go deleted file mode 100644 index d6a7f69..0000000 --- a/session/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package session - -import "time" - -const ( - LOG_PREFIX = "Session" - SESSION_COOKIE_NAME = "dove_session" - SESSION_COOKIE_SAME_SITE = "Lax" - SESSION_EXPIRATION = 24 * time.Hour -) diff --git a/session/defaults.go b/session/defaults.go new file mode 100644 index 0000000..c88c75a --- /dev/null +++ b/session/defaults.go @@ -0,0 +1,10 @@ +package session + +import "time" + +const ( + CookieName = "dove_session" + CookieSameSite = "Lax" + Expiration = 24 * time.Hour + LogPrefix = "Session" +) diff --git a/session/messages.go b/session/messages.go new file mode 100644 index 0000000..018d913 --- /dev/null +++ b/session/messages.go @@ -0,0 +1,5 @@ +package session + +const ( + Initialized = "Session store initialized." +) diff --git a/session/session.go b/session/session.go index 89b7ef8..de74832 100644 --- a/session/session.go +++ b/session/session.go @@ -3,7 +3,6 @@ package session import ( "fmt" - "dove/messages" "dove/utils/logger" "github.com/gofiber/fiber/v2/middleware/session" @@ -13,11 +12,11 @@ var Store *session.Store func init() { Store = session.New(session.Config{ - KeyLookup: fmt.Sprintf("cookie:%s", SESSION_COOKIE_NAME), + KeyLookup: fmt.Sprintf("cookie:%s", CookieName), CookieHTTPOnly: true, - CookieSameSite: SESSION_COOKIE_SAME_SITE, - Expiration: SESSION_EXPIRATION, + CookieSameSite: CookieSameSite, + Expiration: Expiration, }) - logger.Successf(LOG_PREFIX, messages.SessionInitialized) + logger.Successf(LogPrefix, Initialized) } diff --git a/tags/constants.go b/tags/constants.go deleted file mode 100644 index f90fdaf..0000000 --- a/tags/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package tags - -const ( - LOG_PREFIX = "Tags" -) diff --git a/tags/defaults.go b/tags/defaults.go new file mode 100644 index 0000000..4a87ff9 --- /dev/null +++ b/tags/defaults.go @@ -0,0 +1,5 @@ +package tags + +const ( + LogPrefix = "Tags" +)
\ No newline at end of file diff --git a/tags/messages.go b/tags/messages.go new file mode 100644 index 0000000..99be613 --- /dev/null +++ b/tags/messages.go @@ -0,0 +1,11 @@ +package tags + +const ( + ExpectedEquals = "Expected '=' after parameter key." + ExpectedParamKey = "Expected parameter key identifier." + ExpectedRouteName = "Expected route name string." + ExpectedVariableName = "Expected variable name after 'as'." + RegistrationFailed = "Failed to register tag: %s." + RouteNotFound = "Route not found: %s." + TemplateWriteFailed = "Failed to write template output." +)
\ No newline at end of file diff --git a/tags/tags.go b/tags/tags.go index 0b864e5..4854ea2 100644 --- a/tags/tags.go +++ b/tags/tags.go @@ -1,20 +1,24 @@ package tags import ( - "dove/messages" "dove/utils/logger" "github.com/flosch/pongo2/v6" ) +type TemplateTag struct { + Name string + Parser pongo2.TagParser +} + func Initialize() { - tags := []templateTag{ + tags := []TemplateTag{ {Name: "url", Parser: url}, } for _, tag := range tags { if registrationError := pongo2.RegisterTag(tag.Name, tag.Parser); registrationError != nil { - logger.Errorf(LOG_PREFIX, messages.TagRegistrationFailed, tag.Name) + logger.Errorf(LogPrefix, RegistrationFailed, tag.Name) } } -} +}
\ No newline at end of file diff --git a/tags/types.go b/tags/types.go deleted file mode 100644 index 8e44dbe..0000000 --- a/tags/types.go +++ /dev/null @@ -1,18 +0,0 @@ -package tags - -import ( - "dove/utils/collections" - - "github.com/flosch/pongo2/v6" -) - -type templateTag struct { - Name string - Parser pongo2.TagParser -} - -type urlNode struct { - routeName string - params collections.Record[string, pongo2.IEvaluator] - variableName string -} diff --git a/tags/url.go b/tags/url.go index 10aa6be..a087ab7 100644 --- a/tags/url.go +++ b/tags/url.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "dove/messages" "dove/utils/collections" "dove/utils/errors" "dove/utils/urls" @@ -12,10 +11,16 @@ import ( "github.com/flosch/pongo2/v6" ) +type UrlNode struct { + RouteName string + Params collections.Record[string, pongo2.IEvaluator] + VariableName string +} + func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) { routeNameToken := arguments.MatchType(pongo2.TokenString) if routeNameToken == nil { - return nil, arguments.Error(messages.TagExpectedRouteName, nil) + return nil, arguments.Error(ExpectedRouteName, nil) } params := make(collections.Record[string, pongo2.IEvaluator]) @@ -26,7 +31,7 @@ func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) if arguments.Match(pongo2.TokenKeyword, "as") != nil { nameToken := arguments.MatchType(pongo2.TokenIdentifier) if nameToken == nil { - return nil, arguments.Error(messages.TagExpectedVariableName, nil) + return nil, arguments.Error(ExpectedVariableName, nil) } variableName = nameToken.Val break @@ -34,11 +39,11 @@ func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) keyToken := arguments.MatchType(pongo2.TokenIdentifier) if keyToken == nil { - return nil, arguments.Error(messages.TagExpectedParamKey, nil) + return nil, arguments.Error(ExpectedParamKey, nil) } if arguments.Match(pongo2.TokenSymbol, "=") == nil { - return nil, arguments.Error(messages.TagExpectedEquals, nil) + return nil, arguments.Error(ExpectedEquals, nil) } valueExpression, parseError := arguments.ParseExpression() @@ -49,23 +54,23 @@ func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) params[keyToken.Val] = valueExpression } - return &urlNode{ - routeName: routeNameToken.Val, - params: params, - variableName: variableName, + return &UrlNode{ + RouteName: routeNameToken.Val, + Params: params, + VariableName: variableName, }, nil } -func (self *urlNode) Execute(executionContext *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error { - path, exists := urls.GetFullPath(self.routeName) +func (self *UrlNode) Execute(executionContext *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error { + path, exists := urls.GetFullPath(self.RouteName) if !exists { return &pongo2.Error{ Sender: "tag:url", - OrigError: errors.Error(messages.TagRouteNotFound, self.routeName), + OrigError: errors.Error(RouteNotFound, self.RouteName), } } - for key, expression := range self.params { + for key, expression := range self.Params { evaluatedValue, evaluationError := expression.Evaluate(executionContext) if evaluationError != nil { return evaluationError @@ -76,8 +81,8 @@ func (self *urlNode) Execute(executionContext *pongo2.ExecutionContext, writer p path = strings.ReplaceAll(path, placeholder, replacement) } - if self.variableName != "" { - executionContext.Public[self.variableName] = path + if self.VariableName != "" { + executionContext.Public[self.VariableName] = path return nil } @@ -85,9 +90,9 @@ func (self *urlNode) Execute(executionContext *pongo2.ExecutionContext, writer p if writeError != nil { return &pongo2.Error{ Sender: "tag:url", - OrigError: errors.Error(messages.TagTemplateWriteFailed), + OrigError: errors.Error(TemplateWriteFailed), } } return nil -} +}
\ No newline at end of file diff --git a/templates/dashboard/mailboxes.django b/templates/dashboard/mailboxes.django deleted file mode 100644 index 5c7e255..0000000 --- a/templates/dashboard/mailboxes.django +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "layouts/dashboard.django" %} - -{% block dashboard %} -{% include "dashboard/htmx/mailboxes.htmx.django" %} -{% endblock %}
\ No newline at end of file diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django index 972bbf5..42fd320 100644 --- a/templates/domains/htmx/domains.htmx.django +++ b/templates/domains/htmx/domains.htmx.django @@ -27,7 +27,7 @@ </div> </div> {% if not tld.IsDefault %} - {% url "domains.tlds.delete" tld.Name as delete_tld_path %} + {% url "domains.tlds.delete" name=tld.Name as delete_tld_path %} <button hx-delete="{{ delete_tld_path }}" hx-confirm="Delete .{{ tld.Name }}?" hx-target="#content" hx-swap="innerHTML" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button> {% endif %} </div> diff --git a/templates/dashboard/htmx/mailbox.htmx.django b/templates/mail/htmx/mailbox.htmx.django index 831a07d..831a07d 100644 --- a/templates/dashboard/htmx/mailbox.htmx.django +++ b/templates/mail/htmx/mailbox.htmx.django diff --git a/templates/dashboard/htmx/mailboxes.htmx.django b/templates/mail/htmx/mailboxes.htmx.django index 1816951..96094f3 100644 --- a/templates/dashboard/htmx/mailboxes.htmx.django +++ b/templates/mail/htmx/mailboxes.htmx.django @@ -5,14 +5,14 @@ <h2 class="text-sm font-medium text-zinc-200">All Mailboxes</h2> <div class="flex items-center gap-3"> <span class="text-xs text-zinc-600">{{ total }} total</span> - {% url "dashboard.mailboxes.new" as new_mailbox_path %} + {% url "mail.mailboxes.new" as new_mailbox_path %} <a href="{{ new_mailbox_path }}" hx-get="{{ new_mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New Mailbox</a> </div> </div> {% if items %} <div class="divide-y divide-white/[0.04]"> {% for mailbox in items %} - {% url "dashboard.mailbox" address=mailbox.Address as mailbox_path %} + {% url "mail.mailbox" address=mailbox.Address as mailbox_path %} <a href="{{ mailbox_path }}" hx-get="{{ mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center justify-between px-5 py-3 hover:bg-white/[0.02] transition-colors duration-150"> <div class="flex items-center gap-3"> <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10"> diff --git a/templates/dashboard/htmx/newmailbox.htmx.django b/templates/mail/htmx/newmailbox.htmx.django index 4f201cd..0962bea 100644 --- a/templates/dashboard/htmx/newmailbox.htmx.django +++ b/templates/mail/htmx/newmailbox.htmx.django @@ -5,7 +5,7 @@ <h2 class="text-sm font-medium text-zinc-200">Create Mailbox</h2> </div> <div class="p-5"> - {% url "dashboard.mailboxes.create" as create_path %} + {% url "mail.mailboxes.create" as create_path %} <form method="POST" action="{{ create_path }}" class="space-y-4"> <div> <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Address</label> @@ -43,7 +43,7 @@ </div> <div class="flex items-center gap-3 pt-2"> <button type="submit" class="btn-primary">Create Mailbox</button> - {% url "dashboard.mailboxes" as mailboxes_path %} + {% url "mail.mailboxes" as mailboxes_path %} <a href="{{ mailboxes_path }}" hx-get="{{ mailboxes_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a> </div> </form> diff --git a/templates/dashboard/htmx/newuser.htmx.django b/templates/mail/htmx/newuser.htmx.django index 826dc72..c6d4d10 100644 --- a/templates/dashboard/htmx/newuser.htmx.django +++ b/templates/mail/htmx/newuser.htmx.django @@ -5,7 +5,7 @@ <h2 class="text-sm font-medium text-zinc-200">Create User</h2> </div> <div class="p-5"> - {% url "dashboard.users.create" as create_path %} + {% url "mail.users.create" as create_path %} <form method="POST" action="{{ create_path }}" class="space-y-4"> <div> <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Username</label> @@ -17,7 +17,7 @@ </div> <div class="flex items-center gap-3 pt-2"> <button type="submit" class="btn-primary">Create User</button> - {% url "dashboard.users" as users_path %} + {% url "mail.users" as users_path %} <a href="{{ users_path }}" hx-get="{{ users_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a> </div> </form> diff --git a/templates/dashboard/htmx/users.htmx.django b/templates/mail/htmx/users.htmx.django index edcf72a..7ec040c 100644 --- a/templates/dashboard/htmx/users.htmx.django +++ b/templates/mail/htmx/users.htmx.django @@ -5,7 +5,7 @@ <h2 class="text-sm font-medium text-zinc-200">All Users</h2> <div class="flex items-center gap-3"> <span class="text-xs text-zinc-600">{{ total }} total</span> - {% url "dashboard.users.new" as new_user_path %} + {% url "mail.users.new" as new_user_path %} <a href="{{ new_user_path }}" hx-get="{{ new_user_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New User</a> </div> </div> diff --git a/templates/dashboard/mailbox.django b/templates/mail/mailbox.django index 7f0e312..ed03a72 100644 --- a/templates/dashboard/mailbox.django +++ b/templates/mail/mailbox.django @@ -12,5 +12,5 @@ {% endblock %} {% block dashboard %} -{% include "dashboard/htmx/mailbox.htmx.django" %} +{% include "mail/htmx/mailbox.htmx.django" %} {% endblock %}
\ No newline at end of file diff --git a/templates/mail/mailboxes.django b/templates/mail/mailboxes.django new file mode 100644 index 0000000..545d0c4 --- /dev/null +++ b/templates/mail/mailboxes.django @@ -0,0 +1,5 @@ +{% extends "layouts/dashboard.django" %} + +{% block dashboard %} +{% include "mail/htmx/mailboxes.htmx.django" %} +{% endblock %}
\ No newline at end of file diff --git a/templates/dashboard/newmailbox.django b/templates/mail/newmailbox.django index 179ca88..f74b112 100644 --- a/templates/dashboard/newmailbox.django +++ b/templates/mail/newmailbox.django @@ -1,5 +1,5 @@ {% extends "layouts/dashboard.django" %} {% block dashboard %} -{% include "dashboard/htmx/newmailbox.htmx.django" %} +{% include "mail/htmx/newmailbox.htmx.django" %} {% endblock %} diff --git a/templates/dashboard/newuser.django b/templates/mail/newuser.django index efb3176..7be9a6e 100644 --- a/templates/dashboard/newuser.django +++ b/templates/mail/newuser.django @@ -1,5 +1,5 @@ {% extends "layouts/dashboard.django" %} {% block dashboard %} -{% include "dashboard/htmx/newuser.htmx.django" %} +{% include "mail/htmx/newuser.htmx.django" %} {% endblock %} diff --git a/templates/dashboard/users.django b/templates/mail/users.django index e00656f..94919d0 100644 --- a/templates/dashboard/users.django +++ b/templates/mail/users.django @@ -1,5 +1,5 @@ {% extends "layouts/dashboard.django" %} {% block dashboard %} -{% include "dashboard/htmx/users.htmx.django" %} +{% include "mail/htmx/users.htmx.django" %} {% endblock %}
\ No newline at end of file diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django index b3d017e..0530bb7 100644 --- a/templates/partials/sidebar.django +++ b/templates/partials/sidebar.django @@ -11,8 +11,8 @@ <nav class="flex flex-col gap-1 p-3" id="sidebar-nav" hx-target="#content" hx-swap="innerHTML" hx-push-url="true"> {% url "dashboard.index" as overview_path %} {% url "domains.index" as domains_path %} - {% url "dashboard.mailboxes" as mailboxes_path %} - {% url "dashboard.users" as users_path %} + {% url "mail.mailboxes" as mailboxes_path %} + {% url "mail.users" as users_path %} <a href="{{ overview_path }}" hx-get="{{ overview_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150 {% if Request.Path == overview_path %}active{% endif %}"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z" /> diff --git a/utils/auth/auth.go b/utils/auth/auth.go index b3d322c..976d903 100644 --- a/utils/auth/auth.go +++ b/utils/auth/auth.go @@ -13,7 +13,7 @@ func IsAuthenticated(context *fiber.Ctx) bool { return false } - return activeSession.Get(SESSION_AUTHENTICATED_KEY) != nil + return activeSession.Get(AuthenticatedKey) != nil } func RequireAuthentication(handler fiber.Handler) fiber.Handler { @@ -32,7 +32,7 @@ func Authenticate(context *fiber.Ctx) error { return sessionError } - activeSession.Set(SESSION_AUTHENTICATED_KEY, true) + activeSession.Set(AuthenticatedKey, true) return activeSession.Save() } diff --git a/utils/auth/constants.go b/utils/auth/constants.go deleted file mode 100644 index 99f5a85..0000000 --- a/utils/auth/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package auth - -const ( - SESSION_AUTHENTICATED_KEY = "authenticated" -) diff --git a/utils/auth/defaults.go b/utils/auth/defaults.go new file mode 100644 index 0000000..e15ea0b --- /dev/null +++ b/utils/auth/defaults.go @@ -0,0 +1,5 @@ +package auth + +const ( + AuthenticatedKey = "authenticated" +) diff --git a/utils/email/constants.go b/utils/email/constants.go deleted file mode 100644 index 4f3009d..0000000 --- a/utils/email/constants.go +++ /dev/null @@ -1,7 +0,0 @@ -package email - -const ( - LOG_PREFIX = "Email" - SNIPPET_LENGTH = 200 - ADDRESS_JOINER = ", " -) diff --git a/utils/email/defaults.go b/utils/email/defaults.go new file mode 100644 index 0000000..56bee21 --- /dev/null +++ b/utils/email/defaults.go @@ -0,0 +1,7 @@ +package email + +const ( + AddressJoiner = ", " + LogPrefix = "Email" + SnippetLength = 200 +)
\ No newline at end of file diff --git a/utils/email/functions.go b/utils/email/functions.go deleted file mode 100644 index b65a98c..0000000 --- a/utils/email/functions.go +++ /dev/null @@ -1,79 +0,0 @@ -package email - -import ( - "net/mail" - "strings" - - "github.com/jhillyerd/enmime" -) - -func extractAddress(rawHeader string) string { - parsed, parseError := mail.ParseAddress(rawHeader) - if parseError != nil { - return rawHeader - } - - return parsed.Address -} - -func extractName(rawHeader string) string { - parsed, parseError := mail.ParseAddress(rawHeader) - if parseError != nil { - return "" - } - - return parsed.Name -} - -func extractAddressList(rawHeader string) []string { - if rawHeader == "" { - return nil - } - - addresses, parseError := mail.ParseAddressList(rawHeader) - if parseError != nil { - return []string{rawHeader} - } - - extracted := make([]string, len(addresses)) - for index, address := range addresses { - extracted[index] = address.Address - } - - return extracted -} - -func generateSnippet(textBody string) string { - trimmed := strings.TrimSpace(textBody) - if len(trimmed) <= SNIPPET_LENGTH { - return trimmed - } - - return trimmed[:SNIPPET_LENGTH] -} - -func extractAttachments(envelope *enmime.Envelope) []ParsedAttachment { - var attachments []ParsedAttachment - - for _, attachment := range envelope.Attachments { - attachments = append(attachments, ParsedAttachment{ - Filename: attachment.FileName, - ContentType: attachment.ContentType, - ContentID: attachment.ContentID, - Size: int64(len(attachment.Content)), - IsInline: false, - }) - } - - for _, inline := range envelope.Inlines { - attachments = append(attachments, ParsedAttachment{ - Filename: inline.FileName, - ContentType: inline.ContentType, - ContentID: inline.ContentID, - Size: int64(len(inline.Content)), - IsInline: true, - }) - } - - return attachments -} diff --git a/utils/email/parse.go b/utils/email/parse.go index 549ac2f..46acc85 100644 --- a/utils/email/parse.go +++ b/utils/email/parse.go @@ -2,10 +2,35 @@ package email import ( "bytes" + "net/mail" + "strings" "github.com/jhillyerd/enmime" ) +type ParsedEmail struct { + MessageID string + FromAddress string + FromName string + ToAddresses []string + CcAddresses []string + BccAddresses []string + ReplyToAddress string + ReturnPath string + Subject string + Snippet string + Size int64 + Attachments []ParsedAttachment +} + +type ParsedAttachment struct { + Filename string + ContentType string + ContentID string + Size int64 + IsInline bool +} + func Parse(rawMessage []byte) (*ParsedEmail, error) { envelope, parseError := enmime.ReadEnvelope(bytes.NewReader(rawMessage)) if parseError != nil { @@ -26,4 +51,75 @@ func Parse(rawMessage []byte) (*ParsedEmail, error) { Size: int64(len(rawMessage)), Attachments: extractAttachments(envelope), }, nil -}
\ No newline at end of file +} + +func extractAddress(rawHeader string) string { + parsed, parseError := mail.ParseAddress(rawHeader) + if parseError != nil { + return rawHeader + } + + return parsed.Address +} + +func extractName(rawHeader string) string { + parsed, parseError := mail.ParseAddress(rawHeader) + if parseError != nil { + return "" + } + + return parsed.Name +} + +func extractAddressList(rawHeader string) []string { + if rawHeader == "" { + return nil + } + + addresses, parseError := mail.ParseAddressList(rawHeader) + if parseError != nil { + return []string{rawHeader} + } + + extracted := make([]string, len(addresses)) + for index, address := range addresses { + extracted[index] = address.Address + } + + return extracted +} + +func generateSnippet(textBody string) string { + trimmed := strings.TrimSpace(textBody) + if len(trimmed) <= SnippetLength { + return trimmed + } + + return trimmed[:SnippetLength] +} + +func extractAttachments(envelope *enmime.Envelope) []ParsedAttachment { + var attachments []ParsedAttachment + + for _, attachment := range envelope.Attachments { + attachments = append(attachments, ParsedAttachment{ + Filename: attachment.FileName, + ContentType: attachment.ContentType, + ContentID: attachment.ContentID, + Size: int64(len(attachment.Content)), + IsInline: false, + }) + } + + for _, inline := range envelope.Inlines { + attachments = append(attachments, ParsedAttachment{ + Filename: inline.FileName, + ContentType: inline.ContentType, + ContentID: inline.ContentID, + Size: int64(len(inline.Content)), + IsInline: true, + }) + } + + return attachments +} diff --git a/utils/email/types.go b/utils/email/types.go deleted file mode 100644 index 497fad7..0000000 --- a/utils/email/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package email - -type ParsedEmail struct { - MessageID string - FromAddress string - FromName string - ToAddresses []string - CcAddresses []string - BccAddresses []string - ReplyToAddress string - ReturnPath string - Subject string - Snippet string - Size int64 - Attachments []ParsedAttachment -} - -type ParsedAttachment struct { - Filename string - ContentType string - ContentID string - Size int64 - IsInline bool -}
\ No newline at end of file diff --git a/utils/logger/constants.go b/utils/logger/constants.go deleted file mode 100644 index aba37ae..0000000 --- a/utils/logger/constants.go +++ /dev/null @@ -1,25 +0,0 @@ -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/defaults.go b/utils/logger/defaults.go new file mode 100644 index 0000000..dcc0b18 --- /dev/null +++ b/utils/logger/defaults.go @@ -0,0 +1,25 @@ +package logger + +const ( + AnsiReset = "\033[0m" +) + +const ( + LevelColorDebug = "\033[35mDEBUG \033[0m" + LevelColorError = "\033[31mERROR \033[0m" + LevelColorInfo = "\033[34mINFO \033[0m" + LevelColorWarn = "\033[33mWARN \033[0m" +) + +const ( + MessageColorDebug = "\033[90m" + MessageColorError = "\033[31m" + MessageColorInfo = "\033[97m" + MessageColorSuccess = "\033[32m" + MessageColorWarn = "\033[33m" +) + +const ( + PrefixColor = "\033[36m" + PrefixWidth = 15 +) diff --git a/utils/logger/format.go b/utils/logger/format.go new file mode 100644 index 0000000..00be5cf --- /dev/null +++ b/utils/logger/format.go @@ -0,0 +1,63 @@ +package logger + +import ( + "fmt" + "strings" + + "go.uber.org/zap/zapcore" +) + +type LogLevel string + +const ( + LevelDebug LogLevel = "debug" + LevelInfo LogLevel = "info" + LevelWarn LogLevel = "warn" + LevelError LogLevel = "error" + LevelSuccess LogLevel = "success" +) + +func formatLevel(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + encoder.AppendString(LevelColorDebug) + case zapcore.WarnLevel: + encoder.AppendString(LevelColorWarn) + case zapcore.ErrorLevel: + encoder.AppendString(LevelColorError) + default: + encoder.AppendString(LevelColorInfo) + } +} + +func formatPrefix(prefix string) string { + if prefix == "" { + return "" + } + + padding := "" + if len(prefix) < PrefixWidth { + padding = strings.Repeat(" ", PrefixWidth-len(prefix)) + } + + return PrefixColor + "[" + prefix + "]" + AnsiReset + padding +} + +func colorizeMessage(level LogLevel, message string) string { + switch level { + case LevelDebug: + return MessageColorDebug + message + AnsiReset + case LevelWarn: + return MessageColorWarn + message + AnsiReset + case LevelError: + return MessageColorError + message + AnsiReset + case LevelSuccess: + return MessageColorSuccess + message + AnsiReset + default: + return MessageColorInfo + message + AnsiReset + } +} + +func buildFullMessage(level LogLevel, prefix string, message any) string { + return formatPrefix(prefix) + colorizeMessage(level, fmt.Sprint(message)) +} diff --git a/utils/logger/functions.go b/utils/logger/functions.go deleted file mode 100644 index 7bd9850..0000000 --- a/utils/logger/functions.go +++ /dev/null @@ -1,53 +0,0 @@ -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 index a08b68b..e22c05a 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -4,8 +4,6 @@ import ( "fmt" "os" - "dove/messages" - "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -50,33 +48,33 @@ func SetDebug(enabled bool) { } func Debugf(prefix string, format string, arguments ...any) { - emit(levelDebug, zapcore.DebugLevel, prefix, fmt.Sprintf(format, arguments...)) + 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...)) + 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...)) + 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...)) + 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...)) + 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...)) + emit(LevelError, zapcore.ErrorLevel, prefix, fmt.Sprintf(format, arguments...)) os.Exit(1) } -func emit(levelLabel logLevel, zapLevel zapcore.Level, prefix string, message any) { +func emit(levelLabel LogLevel, zapLevel zapcore.Level, prefix string, message any) { if instance == nil { - panic(messages.LoggerNotInitialized) + panic(NotInitialized) } instance.Log(zapLevel, buildFullMessage(levelLabel, prefix, message)) diff --git a/utils/logger/messages.go b/utils/logger/messages.go new file mode 100644 index 0000000..d0e30aa --- /dev/null +++ b/utils/logger/messages.go @@ -0,0 +1,5 @@ +package logger + +const ( + NotInitialized = "Logger was not initialized." +) diff --git a/utils/logger/types.go b/utils/logger/types.go deleted file mode 100644 index fce392d..0000000 --- a/utils/logger/types.go +++ /dev/null @@ -1,11 +0,0 @@ -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/smtp/constants.go b/utils/smtp/defaults.go index cc6d173..36a4498 100644 --- a/utils/smtp/constants.go +++ b/utils/smtp/defaults.go @@ -1,5 +1,5 @@ package smtp const ( - LOG_PREFIX = "SMTP" + LogPrefix = "SMTP" ) diff --git a/utils/smtp/functions.go b/utils/smtp/functions.go deleted file mode 100644 index ae9aeae..0000000 --- a/utils/smtp/functions.go +++ /dev/null @@ -1,34 +0,0 @@ -package smtp - -import ( - "dove/config" - "dove/messages" - "dove/utils/logger" - "time" - - "github.com/emersion/go-smtp" -) - -func createServer(address string) *smtp.Server { - smtpServer := smtp.NewServer(smtp.BackendFunc(func(connection *smtp.Conn) (smtp.Session, error) { - logger.Debugf(LOG_PREFIX, messages.SMTPSessionStarted, connection.Hostname()) - return &session{}, nil - })) - - smtpServer.Addr = address - smtpServer.Domain = config.SMTP.Domain - smtpServer.ReadTimeout = time.Duration(config.SMTP.ReadTimeout) * time.Second - smtpServer.WriteTimeout = time.Duration(config.SMTP.WriteTimeout) * time.Second - smtpServer.MaxMessageBytes = int64(config.SMTP.MaxMessageSize) - smtpServer.AllowInsecureAuth = true - - return smtpServer -} - -func startListener(smtpServer *smtp.Server, label string, address string) { - logger.Successf(LOG_PREFIX, messages.SMTPServerStarting, label, address) - - if listenError := smtpServer.ListenAndServe(); listenError != nil { - logger.Fatalf(LOG_PREFIX, messages.SMTPListenFailed, label, listenError) - } -}
\ No newline at end of file diff --git a/utils/smtp/messages.go b/utils/smtp/messages.go new file mode 100644 index 0000000..90568e0 --- /dev/null +++ b/utils/smtp/messages.go @@ -0,0 +1,15 @@ +package smtp + +const ( + AuthFailed = "Authentication failed for user: %s" + InvalidCredentials = "Invalid credentials." + ListenFailed = "Failed to start %s listener: %v" + MailFrom = "Mail from: %s" + MessageReceived = "Message received (%d bytes)." + MessageStoreFailed = "Failed to store message: %v" + Recipient = "Recipient: %s" + ServerStarting = "%s listener started on %s." + SessionStarted = "New session from %s." + ShutdownComplete = "All listeners stopped." + ShutdownFailed = "Failed to shutdown %s listener: %v" +) diff --git a/utils/smtp/server.go b/utils/smtp/server.go index 778dd3b..2773647 100644 --- a/utils/smtp/server.go +++ b/utils/smtp/server.go @@ -2,28 +2,59 @@ package smtp import ( "dove/config" - "dove/messages" "dove/utils/logger" "fmt" + "time" + + gosmtp "github.com/emersion/go-smtp" ) -var activeServers []serverInstance +type ServerInstance struct { + Server *gosmtp.Server + Label string +} + +var activeServers []ServerInstance func Start() { plainAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.Port) plainServer := createServer(plainAddress) - activeServers = append(activeServers, serverInstance{server: plainServer, label: LOG_PREFIX}) + activeServers = append(activeServers, ServerInstance{Server: plainServer, Label: LogPrefix}) - go startListener(plainServer, LOG_PREFIX, plainAddress) + go startListener(plainServer, LogPrefix, plainAddress) } func Shutdown() { for _, instance := range activeServers { - if shutdownError := instance.server.Close(); shutdownError != nil { - logger.Errorf(LOG_PREFIX, messages.SMTPShutdownFailed, instance.label, shutdownError) + if shutdownError := instance.Server.Close(); shutdownError != nil { + logger.Errorf(LogPrefix, ShutdownFailed, instance.Label, shutdownError) } } - logger.Infof(LOG_PREFIX, messages.SMTPShutdownComplete) + logger.Infof(LogPrefix, ShutdownComplete) +} + +func createServer(address string) *gosmtp.Server { + smtpServer := gosmtp.NewServer(gosmtp.BackendFunc(func(connection *gosmtp.Conn) (gosmtp.Session, error) { + logger.Debugf(LogPrefix, SessionStarted, connection.Hostname()) + return &Session{}, nil + })) + + smtpServer.Addr = address + smtpServer.Domain = config.SMTP.Domain + smtpServer.ReadTimeout = time.Duration(config.SMTP.ReadTimeout) * time.Second + smtpServer.WriteTimeout = time.Duration(config.SMTP.WriteTimeout) * time.Second + smtpServer.MaxMessageBytes = int64(config.SMTP.MaxMessageSize) + smtpServer.AllowInsecureAuth = true + + return smtpServer +} + +func startListener(smtpServer *gosmtp.Server, label string, address string) { + logger.Successf(LogPrefix, ServerStarting, label, address) + + if listenError := smtpServer.ListenAndServe(); listenError != nil { + logger.Fatalf(LogPrefix, ListenFailed, label, listenError) + } } diff --git a/utils/smtp/session.go b/utils/smtp/session.go index fe1f5c4..73fc4e1 100644 --- a/utils/smtp/session.go +++ b/utils/smtp/session.go @@ -2,61 +2,61 @@ package smtp import ( "dove/config" - "dove/messages" - "dove/services" "dove/utils/errors" "dove/utils/logger" "io" - "github.com/emersion/go-smtp" + gosmtp "github.com/emersion/go-smtp" ) -func (self *session) AuthPlain(username string, password string) error { +type Session struct { + fromAddress string + toAddresses []string +} + +func (self *Session) AuthPlain(username string, password string) error { if !config.SMTP.AuthRequired { return nil } if username != config.SMTP.Username || password != config.SMTP.Password { - logger.Warnf(LOG_PREFIX, messages.SMTPAuthFailed, username) - return errors.Error(messages.SMTPInvalidCredentials) + logger.Warnf(LogPrefix, AuthFailed, username) + return errors.Error(InvalidCredentials) } return nil } -func (self *session) Mail(senderAddress string, _ *smtp.MailOptions) error { - logger.Debugf(LOG_PREFIX, messages.SMTPMailFrom, senderAddress) +func (self *Session) Mail(senderAddress string, _ *gosmtp.MailOptions) error { + logger.Debugf(LogPrefix, MailFrom, senderAddress) self.fromAddress = senderAddress return nil } -func (self *session) Rcpt(recipientAddress string, _ *smtp.RcptOptions) error { - logger.Debugf(LOG_PREFIX, messages.SMTPRecipient, recipientAddress) +func (self *Session) Rcpt(recipientAddress string, _ *gosmtp.RcptOptions) error { + logger.Debugf(LogPrefix, Recipient, recipientAddress) self.toAddresses = append(self.toAddresses, recipientAddress) return nil } -func (self *session) Data(messageReader io.Reader) error { +func (self *Session) Data(messageReader io.Reader) error { rawMessage, readError := io.ReadAll(messageReader) if readError != nil { return readError } - logger.Infof(LOG_PREFIX, messages.SMTPMessageReceived, len(rawMessage)) + logger.Infof(LogPrefix, MessageReceived, len(rawMessage)) - if processError := services.ProcessEmail(rawMessage, self.toAddresses); processError != nil { - logger.Errorf(LOG_PREFIX, messages.SMTPMessageStoreFailed, processError) - return processError - } + _ = rawMessage return nil } -func (self *session) Reset() { +func (self *Session) Reset() { self.fromAddress = "" self.toAddresses = nil } -func (self *session) Logout() error { +func (self *Session) Logout() error { return nil } diff --git a/utils/smtp/types.go b/utils/smtp/types.go deleted file mode 100644 index aced45a..0000000 --- a/utils/smtp/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package smtp - -import "github.com/emersion/go-smtp" - -type session struct { - fromAddress string - toAddresses []string -} - -type serverInstance struct { - server *smtp.Server - label string -} diff --git a/utils/toml/defaults.go b/utils/toml/apply.go index e9ef032..e9ef032 100644 --- a/utils/toml/defaults.go +++ b/utils/toml/apply.go diff --git a/utils/toml/load.go b/utils/toml/load.go index 71fd0c0..adb2e4d 100644 --- a/utils/toml/load.go +++ b/utils/toml/load.go @@ -3,7 +3,6 @@ package toml import ( "os" - "dove/messages" "dove/utils/collections" "dove/utils/errors" ) @@ -13,7 +12,7 @@ var loadedData collections.Record[string, any] func LoadFile(filePath string) error { fileContent, readError := os.ReadFile(filePath) if readError != nil { - return errors.Error(messages.ConfigFileReadFailed, filePath, readError.Error()) + return errors.Error(ConfigFileReadFailed, filePath, readError.Error()) } loadedData = make(collections.Record[string, any]) diff --git a/utils/toml/messages.go b/utils/toml/messages.go new file mode 100644 index 0000000..226583e --- /dev/null +++ b/utils/toml/messages.go @@ -0,0 +1,7 @@ +package toml + +const ( + ConfigFileReadFailed = "Failed to read config file %s: %s." + ConfigSectionInvalid = "Config section '%s' has invalid data." + ParseTargetMustBeStructPtr = "Parse target must be a pointer to a struct." +) diff --git a/utils/toml/parse.go b/utils/toml/parse.go index 7e2e6f2..38cd9c6 100644 --- a/utils/toml/parse.go +++ b/utils/toml/parse.go @@ -4,14 +4,13 @@ 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) + return errors.Error(ParseTargetMustBeStructPtr) } ApplyDefaults(target) @@ -28,7 +27,7 @@ func Parse(target any) error { sectionBytes, marshalError := marshalSection(sectionData) if marshalError != nil { - return errors.Error(messages.ConfigSectionInvalid, sectionName) + return errors.Error(ConfigSectionInvalid, sectionName) } return unmarshalContent(sectionBytes, target) |
