package main import ( "fmt" "log" "os" "path/filepath" "gopkg.in/yaml.v3" ) type Config struct { SMTP SMTPConfig `yaml:"smtp"` Web WebConfig `yaml:"web"` } type SMTPConfig struct { ListenAddress string `yaml:"listen_address"` ListenPort int `yaml:"listen_port"` StorageDir string `yaml:"storage_dir"` DomainName string `yaml:"domain_name"` MaxMessageSize int64 `yaml:"max_message_size"` EnableAuth bool `yaml:"enable_auth"` } type WebConfig struct { Enabled bool `yaml:"enabled"` Port int `yaml:"port"` Directory string `yaml:"directory"` BasicAuth BasicAuthConfig `yaml:"basic_auth"` } type BasicAuthConfig struct { Enabled bool `yaml:"enabled"` Username string `yaml:"username"` Password string `yaml:"password"` } func getExecutableDir() (string, error) { exe, err := os.Executable() if err != nil { return "", err } return filepath.Dir(exe), nil } func defaultConfig() *Config { return &Config{ SMTP: SMTPConfig{ ListenAddress: "0.0.0.0", ListenPort: 1025, StorageDir: "emails", DomainName: "m.ric930.ru", MaxMessageSize: 50 * 1024 * 1024, EnableAuth: true, }, Web: WebConfig{ Enabled: true, Port: 8089, Directory: "web", BasicAuth: BasicAuthConfig{ Enabled: true, Username: "admin", Password: "admin", }, }, } } func resolveConfigPaths(cfg *Config, exeDir string) { if !filepath.IsAbs(cfg.SMTP.StorageDir) { cfg.SMTP.StorageDir = filepath.Join(exeDir, cfg.SMTP.StorageDir) } if !filepath.IsAbs(cfg.Web.Directory) { cfg.Web.Directory = filepath.Join(exeDir, cfg.Web.Directory) } } func validateConfig(cfg *Config) error { if cfg.SMTP.ListenPort < 1 || cfg.SMTP.ListenPort > 65535 { return fmt.Errorf("invalid smtp.listen_port: %d", cfg.SMTP.ListenPort) } if cfg.SMTP.StorageDir == "" { return fmt.Errorf("smtp.storage_dir must not be empty") } if cfg.SMTP.DomainName == "" { return fmt.Errorf("smtp.domain_name must not be empty") } if cfg.SMTP.MaxMessageSize <= 0 { return fmt.Errorf("smtp.max_message_size must be greater than zero") } if cfg.Web.Enabled { if cfg.Web.Port < 1 || cfg.Web.Port > 65535 { return fmt.Errorf("invalid web.port: %d", cfg.Web.Port) } if cfg.Web.Directory == "" { return fmt.Errorf("web.directory must not be empty") } if cfg.Web.BasicAuth.Enabled { if cfg.Web.BasicAuth.Username == "" { return fmt.Errorf("web.basic_auth.username must not be empty") } if cfg.Web.BasicAuth.Password == "" { return fmt.Errorf("web.basic_auth.password must not be empty") } } } return nil } func LoadConfig(configPath string) (*Config, error) { exeDir, err := getExecutableDir() if err != nil { log.Printf( "Warning: cannot get executable directory: %v, using current directory", err, ) exeDir = "." } finalConfigPath := configPath if !filepath.IsAbs(finalConfigPath) { finalConfigPath = filepath.Join(exeDir, finalConfigPath) } data, err := os.ReadFile(finalConfigPath) if err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("failed to read config: %w", err) } cfg := defaultConfig() cfgData, marshalErr := yaml.Marshal(cfg) if marshalErr != nil { return nil, fmt.Errorf( "failed to create default config data: %w", marshalErr, ) } if writeErr := os.WriteFile(finalConfigPath, cfgData, 0600); writeErr != nil { return nil, fmt.Errorf( "failed to create default config: %w", writeErr, ) } log.Printf("Created default config at %s", finalConfigPath) log.Printf( "WARNING: change the default web Basic Auth password in %s", finalConfigPath, ) resolveConfigPaths(cfg, exeDir) if err := validateConfig(cfg); err != nil { return nil, err } return cfg, nil } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf( "failed to parse YAML config %s: %w", finalConfigPath, err, ) } if err := validateConfig(&cfg); err != nil { return nil, fmt.Errorf( "invalid config %s: %w", finalConfigPath, err, ) } resolveConfigPaths(&cfg, exeDir) return &cfg, nil }