diff --git a/README.md b/README.md index 9d96072..0ce322d 100644 --- a/README.md +++ b/README.md @@ -635,7 +635,7 @@ oo tasks files detach 208 12345 | `cases` | `list`, `create`, `delete`, `member-add` | | `crm-tasks` | `list`, `create`, `delete`, `categories` | | `applications` | `sync` | -| `catalog` | `scan-contacts`, `scan-projects`, `merge`, `match`, `apply` | +| `catalog` | `scan-contacts`, `scan-projects`, `scan-thunderbird`, `merge`, `match`, `apply` | The CLI reads only `.env` from the current working directory (godotenv is a CLI-only concern — the library itself never loads dotfiles). @@ -743,7 +743,8 @@ starts sparse. You need a reviewable inventory before writes. ```bash oo catalog scan-contacts --root /path/to/contacts -O /tmp/contacts.yaml oo catalog scan-projects --root ~/projects -O /tmp/projects.yaml -oo catalog merge -i /tmp/contacts.yaml -i /tmp/projects.yaml -O clients-contacts.yaml +oo catalog scan-thunderbird --root /path/to/thunderbird-profile -O /tmp/thunderbird.yaml +oo catalog merge -i /tmp/contacts.yaml -i /tmp/projects.yaml -i /tmp/thunderbird.yaml -O clients-contacts.yaml oo catalog match -i clients-contacts.yaml # set approve: true on pilot rows in the YAML oo catalog apply --dry-run -i clients-contacts.yaml diff --git a/catalog/thunderbird.go b/catalog/thunderbird.go new file mode 100644 index 0000000..6f1ce0e --- /dev/null +++ b/catalog/thunderbird.go @@ -0,0 +1,252 @@ +package catalog + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + _ "modernc.org/sqlite" +) + +var emailRE = regexp.MustCompile(`(?i)[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}`) + +// noisyEmail reports addresses that should not become CRM catalog persons. +func noisyEmail(email string) bool { + e := NormalizeEmail(email) + if e == "" || !strings.Contains(e, "@") { + return true + } + local, domain, ok := strings.Cut(e, "@") + if !ok { + return true + } + noiseLocal := []string{ + "noreply", "no-reply", "donotreply", "mailer-daemon", "postmaster", + "bounce", "notifications", "newsletter", "unsubscribe", + } + for _, p := range noiseLocal { + if strings.Contains(local, p) { + return true + } + } + noiseDomain := []string{ + "marketplace.amazon.", "reply.github.com", "users.noreply.github.com", + "groups.facebook.com", "googlegroups.com", "yahoogroups.", + "email.apple.com", "amazonses.com", "mailchimp.com", + "sendgrid.net", "mailgun.org", "postmarkapp.com", + "property.booking.com", "transvascular.com", + } + for _, p := range noiseDomain { + if strings.Contains(domain, p) { + return true + } + } + // random marketplace / tracking locals + if len(local) >= 16 && !strings.Contains(local, ".") && !strings.Contains(local, "-") { + if strings.Contains(domain, "amazon.") { + return true + } + } + return false +} + +// ScanThunderbirdRoot finds Thunderbird profiles under root and emits person rows +// from address books (*.mab) and Gloda global-messages-db.sqlite. +func ScanThunderbirdRoot(root string) (*Document, error) { + root = filepath.Clean(root) + st, err := os.Stat(root) + if err != nil { + return nil, err + } + if !st.IsDir() { + return nil, fmt.Errorf("not a directory: %s", root) + } + + var entries []Entry + seenDB := map[string]struct{}{} + seenMAB := map[string]struct{}{} + + err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + name := d.Name() + if d.IsDir() { + switch name { + case "Cache", "cache2", "startupCache", "OfflineCache", "minidumps", + "crashes", "safebrowsing", "thumbnails", "chrome", "extensions", + "node_modules", ".git": + return filepath.SkipDir + } + return nil + } + lower := strings.ToLower(name) + switch { + case lower == "global-messages-db.sqlite": + if _, ok := seenDB[path]; ok { + return nil + } + seenDB[path] = struct{}{} + parsed, perr := parseGlodaContacts(path) + if perr != nil { + entries = append(entries, Entry{ + ID: EntryID("person", "", filepath.Base(path)), + Kind: "person", + Name: "gloda", + Sources: []string{path}, + Zone: "private", + Role: "unknown", + Notes: "gloda_parse_error: " + perr.Error(), + Status: "new", + }) + return nil + } + entries = append(entries, parsed...) + case strings.HasSuffix(lower, ".mab"): + if _, ok := seenMAB[path]; ok { + return nil + } + seenMAB[path] = struct{}{} + parsed, perr := parseMABEmails(path) + if perr != nil { + return nil + } + entries = append(entries, parsed...) + } + return nil + }) + if err != nil { + return nil, err + } + + return MergeDocs(&Document{Entries: entries}), nil +} + +func parseMABEmails(path string) ([]Entry, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + found := emailRE.FindAllString(string(b), -1) + var out []Entry + for _, raw := range found { + em := NormalizeEmail(raw) + if noisyEmail(em) { + continue + } + org, zone, role := classifyMailIdentity("", em) + out = append(out, Entry{ + ID: EntryID("person", em, ""), + Kind: "person", + Emails: []string{em}, + Org: org, + Sources: []string{path}, + Zone: zone, + Role: role, + Approve: false, + Status: "new", + Notes: "thunderbird_mab", + }) + } + return out, nil +} + +func parseGlodaContacts(dbPath string) ([]Entry, error) { + // read-only URI; immutable=1 helps when WAL/shm are missing + dsn := "file:" + dbPath + "?mode=ro&_pragma=query_only(1)" + db, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, err + } + defer db.Close() + + rows, err := db.Query(` + SELECT COALESCE(c.name, ''), i.value + FROM identities i + LEFT JOIN contacts c ON c.id = i.contactID + WHERE lower(i.kind) = 'email' AND i.value IS NOT NULL AND i.value != '' + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []Entry + for rows.Next() { + var name, value string + if err := rows.Scan(&name, &value); err != nil { + return out, err + } + em := NormalizeEmail(value) + // Gloda sometimes stores "Name " in value + if m := emailRE.FindString(em); m != "" { + em = NormalizeEmail(m) + } + if noisyEmail(em) { + continue + } + display := strings.TrimSpace(name) + // strip wrapping quotes / email leftovers + display = strings.Trim(display, `"'`) + if idx := strings.Index(display, "<"); idx > 0 { + display = strings.TrimSpace(display[:idx]) + } + first, last := "", "" + if display != "" { + first, last = SplitDisplayName(display) + } + org, zone, role := classifyMailIdentity(display, em) + out = append(out, Entry{ + ID: EntryID("person", em, display), + Kind: "person", + Name: display, + First: first, + Last: last, + Emails: []string{em}, + Org: org, + Sources: []string{dbPath}, + Zone: zone, + Role: role, + Approve: false, + Status: "new", + Notes: "thunderbird_gloda", + }) + } + return out, rows.Err() +} + +func classifyMailIdentity(name, email string) (org, zone, role string) { + em := NormalizeEmail(email) + _, domain, _ := strings.Cut(em, "@") + switch { + case domain == "wheregroup.com" || strings.Contains(strings.ToLower(name), "wheregroup"): + return "WhereGroup", "warm", "work" + case domain == "produktor.io" || domain == "eslider.de" || strings.HasSuffix(domain, ".produktor.io"): + return "produktor.io", "hot", "work" + case domain == "dyvenia.com": + return "Dyvenia", "warm", "work" + case domain == "immowelt.de" || domain == "immowelt.com": + return "Immowelt", "warm", "work" + case strings.HasSuffix(domain, ".de") && looksPublicSector(domain): + return domain, "warm", "work" + default: + return "", "private", "unknown" + } +} + +func looksPublicSector(domain string) bool { + d := strings.ToLower(domain) + hints := []string{ + "stadt-", "stadt.", "kreis-", "gemeinde", "landkreis", + "bund.", "bundes", "lvermgeo", "rvr.", "eba.", + } + for _, h := range hints { + if strings.Contains(d, h) { + return true + } + } + return false +} diff --git a/catalog/thunderbird_test.go b/catalog/thunderbird_test.go new file mode 100644 index 0000000..1b366f7 --- /dev/null +++ b/catalog/thunderbird_test.go @@ -0,0 +1,97 @@ +package catalog + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + + _ "modernc.org/sqlite" +) + +func TestNoisyEmail(t *testing.T) { + if !noisyEmail("noreply@example.com") { + t.Fatal("expected noisy") + } + if !noisyEmail("x@marketplace.amazon.de") { + t.Fatal("amazon marketplace") + } + if noisyEmail("andriy.oblivantsev@wheregroup.com") { + t.Fatal("should keep wheregroup") + } +} + +func TestParseMABEmails(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "abook.mab") + body := `// mork junk + PrimaryEmail=andriy.oblivantsev@wheregroup.com + noreply@github.com + axel.schaefer@wheregroup.com +` + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + ents, err := parseMABEmails(path) + if err != nil { + t.Fatal(err) + } + if len(ents) != 2 { + t.Fatalf("got %d: %+v", len(ents), ents) + } + for _, e := range ents { + if e.Org != "WhereGroup" || e.Role != "work" { + t.Fatalf("%+v", e) + } + } +} + +func TestParseGlodaContacts(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "global-messages-db.sqlite") + db, err := sql.Open("sqlite", dbPath) + if err != nil { + t.Fatal(err) + } + _, err = db.Exec(` + CREATE TABLE contacts (id INTEGER PRIMARY KEY, name TEXT); + CREATE TABLE identities (id INTEGER PRIMARY KEY, contactID INTEGER, kind TEXT, value TEXT); + INSERT INTO contacts VALUES (1, 'Axel Schaefer'); + INSERT INTO identities VALUES (1, 1, 'email', 'axel.schaefer@wheregroup.com'); + INSERT INTO contacts VALUES (2, 'Noise Bot'); + INSERT INTO identities VALUES (2, 2, 'email', 'noreply@example.com'); + `) + if err != nil { + t.Fatal(err) + } + _ = db.Close() + + ents, err := parseGlodaContacts(dbPath) + if err != nil { + t.Fatal(err) + } + if len(ents) != 1 { + t.Fatalf("got %d %+v", len(ents), ents) + } + if ents[0].First != "Axel" || ents[0].Emails[0] != "axel.schaefer@wheregroup.com" { + t.Fatalf("%+v", ents[0]) + } +} + +func TestScanThunderbirdRoot(t *testing.T) { + root := t.TempDir() + prof := filepath.Join(root, "profile") + if err := os.MkdirAll(prof, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(prof, "abook.mab"), []byte("mail=paul.schmidt@wheregroup.com\n"), 0o644); err != nil { + t.Fatal(err) + } + doc, err := ScanThunderbirdRoot(root) + if err != nil { + t.Fatal(err) + } + if len(doc.Entries) < 1 { + t.Fatal(doc.Entries) + } +} diff --git a/cmd/oo/catalog.go b/cmd/oo/catalog.go index cdfe541..7b01c04 100644 --- a/cmd/oo/catalog.go +++ b/cmd/oo/catalog.go @@ -18,20 +18,21 @@ trees, match against live OnlyOffice CRM, then create/update only rows with approve: true. Typical flow: - oo catalog scan-contacts --root PATH -o /tmp/contacts.yaml - oo catalog scan-projects --root ~/projects -o /tmp/projects.yaml - oo catalog merge -i /tmp/contacts.yaml -i /tmp/projects.yaml -o docs/catalog/clients-contacts.yaml - oo catalog match -i docs/catalog/clients-contacts.yaml -o docs/catalog/clients-contacts.yaml + oo catalog scan-contacts --root PATH -O /tmp/contacts.yaml + oo catalog scan-projects --root ~/projects -O /tmp/projects.yaml + oo catalog scan-thunderbird --root PATH -O /tmp/thunderbird.yaml + oo catalog merge -i /tmp/contacts.yaml -i /tmp/projects.yaml -i /tmp/thunderbird.yaml -O docs/catalog/clients-contacts.yaml + oo catalog match -i docs/catalog/clients-contacts.yaml # edit approve: true on pilot rows oo catalog apply --dry-run -i docs/catalog/clients-contacts.yaml - oo catalog apply --apply -i docs/catalog/clients-contacts.yaml -o docs/catalog/clients-contacts.yaml -`, -} + oo catalog apply --apply -i docs/catalog/clients-contacts.yaml +`} func init() { rootCmd.AddCommand(catalogCmd) catalogCmd.AddCommand(catalogScanContactsCmd()) catalogCmd.AddCommand(catalogScanProjectsCmd()) + catalogCmd.AddCommand(catalogScanThunderbirdCmd()) catalogCmd.AddCommand(catalogMergeCmd()) catalogCmd.AddCommand(catalogMatchCmd()) catalogCmd.AddCommand(catalogApplyCmd()) @@ -85,6 +86,35 @@ func catalogScanProjectsCmd() *cobra.Command { return cmd } +func catalogScanThunderbirdCmd() *cobra.Command { + var outPath string + cmd := &cobra.Command{ + Use: "scan-thunderbird", + Short: "Thunderbird profiles: abook/history.mab + Gloda SQLite contacts", + Long: `Walk a directory tree for Thunderbird profiles and extract person rows from: + - *.mab address books (email regex) + - global-messages-db.sqlite Gloda contacts/identities + +Noisy senders (noreply, Amazon marketplace, GitHub reply, …) are skipped. +Default zone is private; known work domains (e.g. wheregroup.com) get zone=warm role=work.`, + RunE: func(cmd *cobra.Command, args []string) error { + root, _ := cmd.Flags().GetString("root") + if root == "" { + return fmt.Errorf("--root is required") + } + doc, err := catalog.ScanThunderbirdRoot(root) + if err != nil { + return err + } + return writeCatalog(cmd, doc, outPath) + }, + } + cmd.Flags().String("root", "", "Thunderbird profile or parent directory (local path)") + cmd.Flags().StringVarP(&outPath, "out", "O", "", "write YAML to this path") + _ = cmd.MarkFlagRequired("root") + return cmd +} + func catalogMergeCmd() *cobra.Command { var inputs []string var outPath string diff --git a/cmd/oo/main.go b/cmd/oo/main.go index 6dd3856..4300879 100644 --- a/cmd/oo/main.go +++ b/cmd/oo/main.go @@ -15,7 +15,7 @@ // oo crm cleanup // oo mails accounts | folders | list | get | delete // oo applications sync -// oo catalog scan-contacts | scan-projects | merge | match | apply +// oo catalog scan-contacts | scan-projects | scan-thunderbird | merge | match | apply // // Every list supports `--output/-o json|table` (table is the default). // diff --git a/go.mod b/go.mod index 1ab28e8..6094400 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/muesli/termenv v0.16.0 github.com/spf13/cobra v1.10.2 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.54.0 ) require ( @@ -27,6 +28,8 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect @@ -36,13 +39,18 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect + modernc.org/libc v1.74.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 165c3d5..6c0a467 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emersion/go-vcard v0.0.0-20260618161152-d854b7e0e2d3 h1:B9YK+Tck5mTccyDhtxBzWyqGYcFxLyB6+noMNW4/VgI= github.com/emersion/go-vcard v0.0.0-20260618161152-d854b7e0e2d3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/eslider/go-xls/v2 v2.1.0 h1:HszWKqYQbXxACmAXXWdMsfNl1NDBfGVBnJUPtyUHQ7A= @@ -41,8 +43,14 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -68,8 +76,12 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -89,18 +101,50 @@ github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= +modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= +modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= +modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=