Files
2026-08-12 12:56:22 +01:00

135 lines
3.7 KiB
Go

// 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)
}