135 lines
3.7 KiB
Go
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)
|
|
}
|