479c27e5a8
Phase 2a: userstore domain_admin/superadmin Rollen, User.TenantID,
ListByTenant, UpsertLDAPUser mit tenantID
Phase 2b: storage.Save() mit tenantID *int64, email_refs Tabelle,
GetTenantForMail, GetAllIDsByTenant, StatsByTenant
Phase 2c: JWT-Claims tenant_id/tenant_slug, Session.TenantID,
Login Domain-Erkennung via E-Mail-Domain
Phase 3: tenantMiddleware, Handler-Filterung (Users, Mail, Stats)
Phase 5: SMTP Domain-Routing via DomainToTenantFunc Callback,
config smtp.tenant_routing + default_tenant_id
Phase 8: archivmail migrate-tenants Subkommando
PROJ-2: Upload-Seite /admin/upload mit DropZone + Progress-Polling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
3.4 KiB
Go
155 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/archivmail/config"
|
|
"github.com/archivmail/internal/index"
|
|
"github.com/archivmail/internal/storage"
|
|
"github.com/archivmail/pkg/mailparser"
|
|
)
|
|
|
|
func main() {
|
|
configPath := flag.String("config", "/etc/archivmail/config.yml", "path to config file")
|
|
flag.Parse()
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(os.Stderr, "usage: archivmail-import --config <path> <directory-or-file>")
|
|
os.Exit(1)
|
|
}
|
|
target := args[0]
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
|
|
cfg, err := config.Load(*configPath)
|
|
if err != nil {
|
|
logger.Error("failed to load config", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
mailStore, err := storage.New(storage.Config{
|
|
Dir: cfg.Storage.StorePath,
|
|
Keyfile: cfg.Storage.Keyfile,
|
|
DSN: cfg.Database.DSN(),
|
|
})
|
|
if err != nil {
|
|
logger.Error("storage init failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
defer mailStore.Close()
|
|
|
|
indexBackend := cfg.Index.Backend
|
|
if indexBackend == "" {
|
|
indexBackend = "xapian"
|
|
}
|
|
batchSize := cfg.Index.BatchSize
|
|
if batchSize <= 0 {
|
|
batchSize = 100
|
|
}
|
|
idx, err := index.New(cfg.Index.Path, batchSize, indexBackend)
|
|
if err != nil {
|
|
logger.Error("index init failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
defer idx.Close()
|
|
|
|
var emlFiles []string
|
|
|
|
info, err := os.Stat(target)
|
|
if err != nil {
|
|
logger.Error("target not found", "path", target, "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if info.IsDir() {
|
|
err = filepath.Walk(target, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fi.IsDir() && strings.HasSuffix(strings.ToLower(fi.Name()), ".eml") {
|
|
emlFiles = append(emlFiles, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
logger.Error("walk failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
emlFiles = []string{target}
|
|
}
|
|
|
|
logger.Info("found EML files", "count", len(emlFiles))
|
|
|
|
imported := 0
|
|
skipped := 0
|
|
errors := 0
|
|
|
|
for i, path := range emlFiles {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Error("read file failed", "path", path, "err", err)
|
|
errors++
|
|
continue
|
|
}
|
|
|
|
pm, err := mailparser.Parse(raw)
|
|
if err != nil {
|
|
logger.Error("parse failed", "path", path, "err", err)
|
|
errors++
|
|
continue
|
|
}
|
|
|
|
id, err := mailStore.Save(context.Background(), raw, pm.Date, nil)
|
|
if err != nil {
|
|
logger.Error("save failed", "path", path, "err", err)
|
|
errors++
|
|
continue
|
|
}
|
|
|
|
// Build attachment names list
|
|
var attachNames []string
|
|
for _, att := range pm.Attachments {
|
|
attachNames = append(attachNames, att.Filename)
|
|
}
|
|
|
|
doc := index.MailDocument{
|
|
ID: id,
|
|
From: pm.From,
|
|
To: strings.Join(pm.To, " "),
|
|
Subject: pm.Subject,
|
|
Body: pm.TextBody + " " + pm.HTMLBody,
|
|
AttachNames: strings.Join(attachNames, " "),
|
|
HasAttachment: len(pm.Attachments) > 0,
|
|
Date: pm.Date,
|
|
Size: int64(len(raw)),
|
|
}
|
|
|
|
if err := idx.IndexSync(doc); err != nil {
|
|
logger.Error("index failed", "id", id, "err", err)
|
|
errors++
|
|
continue
|
|
}
|
|
|
|
imported++
|
|
if (i+1)%100 == 0 || i+1 == len(emlFiles) {
|
|
fmt.Printf("Progress: %d/%d (imported: %d, skipped: %d, errors: %d)\n",
|
|
i+1, len(emlFiles), imported, skipped, errors)
|
|
}
|
|
}
|
|
|
|
logger.Info("import complete",
|
|
"total", len(emlFiles),
|
|
"imported", imported,
|
|
"skipped", skipped,
|
|
"errors", errors,
|
|
)
|
|
}
|