Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 59s
Tests / Test (Go stable) (pull_request) Successful in 1m7s
Showroom-safe: the tree no longer carries internal hosts, IPs, ports, personal names, client domains or client file names. Behaviour is unchanged and now supplied per deployment. - catalog: hardcoded mail-org / project classifiers become a YAML-driven Classifier (OO_CATALOG_CONFIG or --config); neutral default classifies nothing as work. New catalog/classify.go + example + tests. - storage_fallback: drop the baked-in MinIO endpoint IP; require MINIO_ENDPOINT (+ keys) from the env. - kontolink: build DocEditor links from $ONLYOFFICE_URL instead of a hardcoded portal host; kontoblatt: no client file id in the output name. - oo: load .env CLI-wide (bootstrap.LoadEnv in execute) so non-authenticating commands (catalog scans) also see config. - genericize comments/docs/fixtures (AGENTS, README, .env.example, crm-associations, catalog tests, ES/pdfattach tests, mails).
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package catalog
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Classifier maps project trees and mail identities to catalog org/zone/role.
|
|
//
|
|
// The library ships with neutral defaults: nothing is classified as work unless
|
|
// the deployment supplies rules. Those rules are deployment-specific, so they
|
|
// live in a YAML config file (path from OO_CATALOG_CONFIG or the --config flag),
|
|
// not in the code. See catalog/classify.example.yaml.
|
|
type Classifier struct {
|
|
// WorkRemotes: a git remote containing any of these substrings → work/hot.
|
|
WorkRemotes []string `yaml:"work_remotes,omitempty"`
|
|
// WorkNames: a project name containing any of these substrings → work/warm.
|
|
WorkNames []string `yaml:"work_names,omitempty"`
|
|
// MailOrgs: ordered mail-identity rules; the first match wins.
|
|
MailOrgs []MailRule `yaml:"mail_orgs,omitempty"`
|
|
}
|
|
|
|
// MailRule maps an email domain and/or a display-name substring to an org with
|
|
// a zone/role. At least one of Domain, Suffix or Name must be set.
|
|
type MailRule struct {
|
|
Domain string `yaml:"domain,omitempty"` // exact domain, case-insensitive
|
|
Suffix string `yaml:"suffix,omitempty"` // domain suffix, e.g. ".example.com"
|
|
Name string `yaml:"name,omitempty"` // substring of the display name
|
|
Org string `yaml:"org"`
|
|
Zone string `yaml:"zone,omitempty"` // default "hot"
|
|
Role string `yaml:"role,omitempty"` // default "work"
|
|
}
|
|
|
|
// DefaultClassifier returns the neutral classifier (no deployment rules).
|
|
func DefaultClassifier() *Classifier { return &Classifier{} }
|
|
|
|
// LoadClassifier reads a classifier config from a YAML file.
|
|
func LoadClassifier(path string) (*Classifier, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var c Classifier
|
|
if err := yaml.Unmarshal(b, &c); err != nil {
|
|
return nil, fmt.Errorf("parse classifier config %s: %w", path, err)
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// LoadClassifierFromEnv loads the classifier named by OO_CATALOG_CONFIG. An
|
|
// empty variable yields the neutral classifier.
|
|
func LoadClassifierFromEnv() (*Classifier, error) {
|
|
path := strings.TrimSpace(os.Getenv("OO_CATALOG_CONFIG"))
|
|
if path == "" {
|
|
return DefaultClassifier(), nil
|
|
}
|
|
return LoadClassifier(path)
|
|
}
|
|
|
|
// ClassifyProject returns (role, zone) for a project name and git remote.
|
|
// Generic name heuristics come first; deployment rules supply the work cases.
|
|
func (c *Classifier) ClassifyProject(name, remote string) (role, zone string) {
|
|
lower := strings.ToLower(name)
|
|
remoteL := strings.ToLower(remote)
|
|
switch {
|
|
case strings.Contains(lower, "experiment") || strings.HasPrefix(lower, "test"):
|
|
return "experiment", "cold"
|
|
case lower == "mama" || lower == "personal" || strings.Contains(lower, "private"):
|
|
return "personal", "private"
|
|
}
|
|
if c != nil {
|
|
for _, r := range c.WorkRemotes {
|
|
if r != "" && strings.Contains(remoteL, strings.ToLower(r)) {
|
|
return "work", "hot"
|
|
}
|
|
}
|
|
for _, n := range c.WorkNames {
|
|
if n != "" && strings.Contains(lower, strings.ToLower(n)) {
|
|
return "work", "warm"
|
|
}
|
|
}
|
|
}
|
|
return "unknown", "warm"
|
|
}
|
|
|
|
// ClassifyMail returns (org, zone, role) for a mail identity. name is the
|
|
// display name (may be empty); email is the address.
|
|
func (c *Classifier) ClassifyMail(name, email string) (org, zone, role string) {
|
|
em := NormalizeEmail(email)
|
|
_, domain, _ := strings.Cut(em, "@")
|
|
nameL := strings.ToLower(strings.TrimSpace(name))
|
|
if c != nil {
|
|
for _, r := range c.MailOrgs {
|
|
if !mailRuleMatches(r, domain, nameL) {
|
|
continue
|
|
}
|
|
z, ro := r.Zone, r.Role
|
|
if z == "" {
|
|
z = "hot"
|
|
}
|
|
if ro == "" {
|
|
ro = "work"
|
|
}
|
|
return r.Org, z, ro
|
|
}
|
|
}
|
|
if strings.HasSuffix(domain, ".de") && looksPublicSector(domain) {
|
|
return domain, "warm", "work"
|
|
}
|
|
return "", "private", "unknown"
|
|
}
|
|
|
|
func mailRuleMatches(r MailRule, domain, nameL string) bool {
|
|
if r.Domain != "" && domain == strings.ToLower(strings.TrimSpace(r.Domain)) {
|
|
return true
|
|
}
|
|
if r.Suffix != "" && strings.HasSuffix(domain, strings.ToLower(strings.TrimSpace(r.Suffix))) {
|
|
return true
|
|
}
|
|
if r.Name != "" && nameL != "" && strings.Contains(nameL, strings.ToLower(strings.TrimSpace(r.Name))) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|