package config import ( "bufio" "fmt" "os" "path/filepath" "strconv" "strings" "time" ) type Config struct { AccountID string POP3 POP3Config SMTP SMTPConfig Data DataConfig HTTP HTTPConfig } type POP3Config struct { Host string Port int Username string Password string UseTLS bool TimeoutSeconds int } type SMTPConfig struct { Host string Port int Username string Password string UseTLS bool UseSTARTTLS bool AuthRequired bool AllowInsecureAuth bool FromAddress string FromName string TimeoutSeconds int } type DataConfig struct { DataDir string DatabasePath string RawMailDir string AttachmentDir string } type HTTPConfig struct { Host string Port int } func (c POP3Config) Timeout() time.Duration { return time.Duration(c.TimeoutSeconds) * time.Second } func (c SMTPConfig) Timeout() time.Duration { return time.Duration(c.TimeoutSeconds) * time.Second } func Load(envPath string) (*Config, error) { values := map[string]string{} if envPath == "" { envPath = ".env" } if err := readEnvFile(envPath, values); err != nil { return nil, err } for _, item := range os.Environ() { key, val, ok := strings.Cut(item, "=") if ok { values[key] = val } } dataDir := getString(values, "DATA_DIR", "./data") cfg := &Config{ AccountID: getString(values, "MAIL_ACCOUNT_ID", "default"), POP3: POP3Config{ Host: getString(values, "POP3_HOST", ""), Port: getInt(values, "POP3_PORT", 110), Username: getString(values, "POP3_USERNAME", ""), Password: getString(values, "POP3_PASSWORD", ""), UseTLS: getBool(values, "POP3_USE_TLS", false), TimeoutSeconds: getInt(values, "POP3_TIMEOUT_SECONDS", 30), }, SMTP: SMTPConfig{ Host: getString(values, "SMTP_HOST", ""), Port: getInt(values, "SMTP_PORT", 25), Username: getString(values, "SMTP_USERNAME", ""), Password: getString(values, "SMTP_PASSWORD", ""), UseTLS: getBool(values, "SMTP_USE_TLS", false), UseSTARTTLS: getBool(values, "SMTP_USE_STARTTLS", false), AuthRequired: getBool(values, "SMTP_AUTH_REQUIRED", false), AllowInsecureAuth: getBool(values, "SMTP_ALLOW_INSECURE_AUTH", false), FromAddress: getString(values, "SMTP_FROM_ADDRESS", ""), FromName: getString(values, "SMTP_FROM_NAME", ""), TimeoutSeconds: getInt(values, "SMTP_TIMEOUT_SECONDS", 30), }, Data: DataConfig{ DataDir: dataDir, DatabasePath: getString(values, "DATABASE_PATH", filepath.Join(dataDir, "internal_mail.db")), RawMailDir: getString(values, "RAW_MAIL_DIR", filepath.Join(dataDir, "raw")), AttachmentDir: getString(values, "ATTACHMENT_DIR", filepath.Join(dataDir, "attachments")), }, HTTP: HTTPConfig{ Host: getString(values, "HTTP_HOST", "127.0.0.1"), Port: getInt(values, "HTTP_PORT", 8765), }, } return cfg, nil } func readEnvFile(path string, values map[string]string) error { file, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("read env file: %w", err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) value = strings.TrimSpace(value) value = strings.Trim(value, `"'`) values[key] = value } return scanner.Err() } func getString(values map[string]string, key, def string) string { if v, ok := values[key]; ok { return v } return def } func getInt(values map[string]string, key string, def int) int { v, ok := values[key] if !ok || strings.TrimSpace(v) == "" { return def } n, err := strconv.Atoi(v) if err != nil { return def } return n } func getBool(values map[string]string, key string, def bool) bool { v, ok := values[key] if !ok || strings.TrimSpace(v) == "" { return def } switch strings.ToLower(strings.TrimSpace(v)) { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false default: return def } }