// Package toolbox provides small shared helpers, mirroring email_server/tool_box.py: // logging, the configured-timezone clock, and Message-ID generation. package toolbox import ( "crypto/rand" "fmt" "log" "math/big" "os" "strings" "time" "gopkg.in/ini.v1" ) // Logger is a tiny leveled logger matching the Python format: // "%(asctime)s - %(name)s - %(levelname)s - %(message)s". type Logger struct { name string level Level out *log.Logger } type Level int const ( LevelDebug Level = iota LevelInfo LevelWarning LevelError LevelCritical ) func parseLevel(s string) Level { switch strings.ToUpper(strings.TrimSpace(s)) { case "DEBUG": return LevelDebug case "WARNING", "WARN": return LevelWarning case "ERROR": return LevelError case "CRITICAL": return LevelCritical default: return LevelInfo } } func (l Level) String() string { switch l { case LevelDebug: return "DEBUG" case LevelWarning: return "WARNING" case LevelError: return "ERROR" case LevelCritical: return "CRITICAL" default: return "INFO" } } var globalLevel = LevelInfo // Configure sets the process-wide log level from settings.ini's [Logging] section, // mirroring tool_box.setup_logging. func Configure(cfg *ini.File) { section := cfg.Section("Logging") globalLevel = parseLevel(section.Key("LOG_LEVEL").MustString("INFO")) } // GetLogger returns a Logger for the given component name, mirroring tool_box.get_logger. // Python derives the name from the caller's filename when omitted; Go callers pass it // explicitly instead, since introspecting the caller module isn't idiomatic here. func GetLogger(name string) *Logger { return &Logger{name: name, level: globalLevel, out: log.New(os.Stderr, "", 0)} } func (l *Logger) log(level Level, format string, args ...any) { if level < globalLevel { return } msg := fmt.Sprintf(format, args...) ts := time.Now().Format("2006-01-02 15:04:05,000") l.out.Printf("%s - %s - %s - %s", ts, l.name, level, msg) } func (l *Logger) Debug(format string, args ...any) { l.log(LevelDebug, format, args...) } func (l *Logger) Info(format string, args ...any) { l.log(LevelInfo, format, args...) } func (l *Logger) Warning(format string, args ...any) { l.log(LevelWarning, format, args...) } func (l *Logger) Error(format string, args ...any) { l.log(LevelError, format, args...) } func (l *Logger) Critical(format string, args ...any) { l.log(LevelCritical, format, args...) } // EnsureFolderExists creates the parent directory of filepath (a file path, not a // directory path), mirroring tool_box.ensure_folder_exists including its handling of // "sqlite:///" prefixed database URLs. func EnsureFolderExists(path string) error { path = strings.TrimPrefix(path, "sqlite:///") dir := path if idx := strings.LastIndexAny(path, "/\\"); idx >= 0 { dir = path[:idx] } else { return nil } if dir == "" { return nil } return os.MkdirAll(dir, 0o755) } // GetCurrentTime returns the current time in the configured server timezone, mirroring // tool_box.get_current_time. Falls back to UTC if the configured zone can't be loaded. func GetCurrentTime(cfg *ini.File) time.Time { tzName := cfg.Section("Server").Key("time_zone").MustString("UTC") loc, err := time.LoadLocation(tzName) if err != nil { loc = time.UTC } return time.Now().In(loc) } // GenerateMessageID builds a Message-ID local-part@hostname string, mirroring // tool_box.generate_message_id: system wall-clock time (not the configured timezone) // plus a 6-digit random suffix. func GenerateMessageID(hostname string) string { digits := make([]byte, 6) for i := range digits { n, _ := rand.Int(rand.Reader, big.NewInt(10)) digits[i] = byte('0' + n.Int64()) } return fmt.Sprintf("%s.%s@%s", time.Now().Format("20060102150405"), string(digits), hostname) }