From d360c9a5ba5fb2f140fb5169dfb348201b58b3c6 Mon Sep 17 00:00:00 2001 From: sysops Date: Sat, 14 Mar 2026 11:43:19 +0100 Subject: [PATCH] feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen - Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg) - Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist - Feature-Status auf In Review gesetzt Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 57 + README.md | 377 +----- cmd/archivmail-export/main.go | 117 ++ cmd/archivmail-import/main.go | 148 ++ cmd/archivmail/main.go | 167 +++ config.test.yml | 35 + config/config.go | 100 ++ docs/PRD.md | 54 +- docs/api-v1.md | 252 ++++ features/INDEX.md | 20 +- .../PROJ-1-authentifizierung-und-rollen.md | 185 +++ features/PROJ-10-admin-bereich.md | 161 +++ features/PROJ-11-audit-log.md | 145 ++ features/PROJ-12-export.md | 122 ++ features/PROJ-13-rest-api-crm.md | 178 +++ features/PROJ-14-import-pop3.md | 147 ++ features/PROJ-15-cli-import-export.md | 195 +++ features/PROJ-16-ldap-active-directory.md | 109 ++ features/PROJ-17-system-dashboard.md | 86 ++ features/PROJ-2-import-eml-mbox.md | 146 ++ features/PROJ-3-import-imap.md | 205 +++ features/PROJ-4-import-smtp.md | 164 +++ .../PROJ-5-speicherung-und-indexierung.md | 194 +++ features/PROJ-6-volltext-suche.md | 128 ++ features/PROJ-7-email-ansicht.md | 184 +++ features/PROJ-8-imap-auto-sync.md | 154 +++ features/PROJ-9-ordner-und-labels.md | 157 +++ go.mod | 26 + go.sum | 77 ++ install.sh | 261 ++++ internal/api/api_test.go | 269 ++++ internal/api/server.go | 1202 +++++++++++++++++ internal/audit/audit.go | 196 +++ internal/audit/audit_test.go | 179 +++ internal/auth/auth.go | 156 +++ internal/auth/auth_test.go | 161 +++ internal/imap/client.go | 99 ++ internal/imap/importer.go | 272 ++++ internal/imap/store.go | 259 ++++ internal/index/index.go | 61 + internal/index/index_test.go | 192 +++ internal/index/xapian.go | 126 ++ internal/index/xapian_stub.go | 9 + internal/index/xapian_wrapper.cpp | 199 +++ internal/index/xapian_wrapper.h | 32 + internal/smtpd/smtpd.go | 283 ++++ internal/storage/storage.go | 145 ++ internal/storage/storage_test.go | 126 ++ internal/userstore/userstore.go | 304 +++++ internal/userstore/userstore_test.go | 279 ++++ next.config.ts | 11 +- package-lock.json | 16 - pkg/mailparser/parser.go | 208 +++ pkg/mailparser/parser_test.go | 100 ++ pkg/mailparser/testdata/multipart.eml | 26 + pkg/mailparser/testdata/simple.eml | 11 + setup.sh | 49 + smoke_test.sh | 144 ++ src/app/admin/page.tsx | 946 +++++++++++++ src/app/imap/page.tsx | 518 +++++++ src/app/layout.tsx | 8 +- src/app/mail/[id]/page.tsx | 352 +++++ src/app/page.tsx | 184 ++- src/app/search/page.tsx | 258 ++++ src/components/navbar.tsx | 71 + src/hooks/useAuth.ts | 46 + src/lib/api.ts | 393 ++++++ update.sh | 132 ++ 68 files changed, 11938 insertions(+), 435 deletions(-) create mode 100644 Makefile create mode 100644 cmd/archivmail-export/main.go create mode 100644 cmd/archivmail-import/main.go create mode 100644 cmd/archivmail/main.go create mode 100644 config.test.yml create mode 100644 config/config.go create mode 100644 docs/api-v1.md create mode 100644 features/PROJ-1-authentifizierung-und-rollen.md create mode 100644 features/PROJ-10-admin-bereich.md create mode 100644 features/PROJ-11-audit-log.md create mode 100644 features/PROJ-12-export.md create mode 100644 features/PROJ-13-rest-api-crm.md create mode 100644 features/PROJ-14-import-pop3.md create mode 100644 features/PROJ-15-cli-import-export.md create mode 100644 features/PROJ-16-ldap-active-directory.md create mode 100644 features/PROJ-17-system-dashboard.md create mode 100644 features/PROJ-2-import-eml-mbox.md create mode 100644 features/PROJ-3-import-imap.md create mode 100644 features/PROJ-4-import-smtp.md create mode 100644 features/PROJ-5-speicherung-und-indexierung.md create mode 100644 features/PROJ-6-volltext-suche.md create mode 100644 features/PROJ-7-email-ansicht.md create mode 100644 features/PROJ-8-imap-auto-sync.md create mode 100644 features/PROJ-9-ordner-und-labels.md create mode 100644 go.mod create mode 100644 go.sum create mode 100755 install.sh create mode 100644 internal/api/api_test.go create mode 100644 internal/api/server.go create mode 100644 internal/audit/audit.go create mode 100644 internal/audit/audit_test.go create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/auth_test.go create mode 100644 internal/imap/client.go create mode 100644 internal/imap/importer.go create mode 100644 internal/imap/store.go create mode 100644 internal/index/index.go create mode 100644 internal/index/index_test.go create mode 100644 internal/index/xapian.go create mode 100644 internal/index/xapian_stub.go create mode 100644 internal/index/xapian_wrapper.cpp create mode 100644 internal/index/xapian_wrapper.h create mode 100644 internal/smtpd/smtpd.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/storage_test.go create mode 100644 internal/userstore/userstore.go create mode 100644 internal/userstore/userstore_test.go create mode 100644 pkg/mailparser/parser.go create mode 100644 pkg/mailparser/parser_test.go create mode 100644 pkg/mailparser/testdata/multipart.eml create mode 100644 pkg/mailparser/testdata/simple.eml create mode 100644 setup.sh create mode 100644 smoke_test.sh create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/imap/page.tsx create mode 100644 src/app/mail/[id]/page.tsx create mode 100644 src/app/search/page.tsx create mode 100644 src/components/navbar.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/lib/api.ts create mode 100755 update.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81a237b --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +APP = archivmail +IMPORTER = archivmail-import +EXPORTER = archivmail-export +VERSION ?= 0.1.0 +LDFLAGS = -X main.Version=$(VERSION) -s -w + +.PHONY: all build clean install + +all: build + +# Build — requires: apt install libxapian-dev +build: + go build -tags xapian -ldflags "$(LDFLAGS)" -o bin/$(APP) ./cmd/archivmail + go build -tags xapian -ldflags "$(LDFLAGS)" -o bin/$(IMPORTER) ./cmd/archivmail-import + go build -tags xapian -ldflags "$(LDFLAGS)" -o bin/$(EXPORTER) ./cmd/archivmail-export + +clean: + rm -rf bin/ + +test: + go test -v -race -count=1 ./internal/storage/... ./internal/userstore/... \ + ./internal/auth/... ./internal/audit/... ./internal/index/... \ + ./pkg/mailparser/... + +test-all: + go test -v -race -count=1 ./... + +test-short: + go test -short ./... + +test-cover: + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report: coverage.html" + +smoke: build + @echo "Starting daemon in background for smoke test..." + mkdir -p /tmp/archivmail-test/{store,xapian,logs} + ./bin/archivmail --config config/config.test.yml & + sleep 2 + bash smoke_test.sh + pkill archivmail || true + +vet: + go vet ./... + +install: build + install -D -m 755 bin/$(APP) /usr/bin/$(APP) + install -D -m 755 bin/$(IMPORTER) /usr/bin/$(IMPORTER) + install -D -m 755 bin/$(EXPORTER) /usr/bin/$(EXPORTER) + install -D -m 644 config/config.yml /etc/archivmail/config.yml + install -D -m 644 debian/archivmail.service /lib/systemd/system/archivmail.service + useradd --system --no-create-home --shell /usr/sbin/nologin archivmail 2>/dev/null || true + mkdir -p /var/archivmail/{store,astore,xapian} + mkdir -p /var/log/archivmail + chown -R archivmail:archivmail /var/archivmail /var/log/archivmail + systemctl daemon-reload diff --git a/README.md b/README.md index 8ee3906..ffdb7f4 100644 --- a/README.md +++ b/README.md @@ -1,323 +1,94 @@ -# AI Coding Starter Kit +# archivmail -> Build production-ready web apps faster with AI-powered Skills handling Requirements, Architecture, Development, QA, and Deployment. +Selbst gehostetes Mail-Archiv-System für Unternehmen. E-Mails werden aus IMAP, SMTP und EML/MBOX-Quellen importiert, volltext-indexiert (Xapian) und verschlüsselt archiviert. -This template uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with modern Skills, Rules, and Sub-Agents to provide a complete AI-powered development workflow. +## Features -## Quick Start +- Import via IMAP, SMTP-Journaling und EML/MBOX-Upload +- Volltext-Suche über Xapian +- AES-256-GCM-Verschlüsselung der archivierten Mails +- Anhang-Deduplizierung +- Rollenmodell: `user`, `auditor`, `admin` +- Audit-Log (PostgreSQL + Append-only Logdatei) -### 1. Clone & Install +## Installation ```bash -git clone https://github.com/YOUR_USERNAME/ai-coding-starter-kit.git my-project -cd my-project -npm install +# Systembenutzer anlegen +useradd -r -s /sbin/nologin archivmail + +# Verzeichnisse anlegen +mkdir -p /etc/archivmail +mkdir -p /var/archivmail/store +mkdir -p /var/archivmail/astore +mkdir -p /var/lib/archivmail/xapian +mkdir -p /var/log/archivmail + +# Berechtigungen setzen +chown -R archivmail:archivmail /var/archivmail /var/lib/archivmail /var/log/archivmail + +# Encryption Key generieren +openssl rand -base64 32 > /etc/archivmail/keyfile +chmod 400 /etc/archivmail/keyfile +chown archivmail:archivmail /etc/archivmail/keyfile + +# Konfiguration +cp config.example.yml /etc/archivmail/config.yml +# config.yml anpassen (Datenbank, SMTP-Port, etc.) + +# Systemd-Service aktivieren +cp archivmail.service /lib/systemd/system/ +systemctl daemon-reload +systemctl enable --now archivmail ``` -### 2. (Optional) Supabase Setup - -If you need a backend: - -1. Create Supabase Project: [supabase.com](https://supabase.com) -2. Copy `.env.local.example` to `.env.local` -3. Add your Supabase credentials -4. Uncomment the Supabase client in `src/lib/supabase.ts` - -Skip this step if you're building frontend-only (landing pages, portfolios, etc.) - -### 3. Start Development - -```bash -npm run dev -``` - -Open [http://localhost:3000](http://localhost:3000) in your browser. - -### 4. Initialize Your Project - -Open Claude Code and describe your project. The `/requirements` skill automatically detects that this is a fresh project and enters **Init Mode**: +## Verzeichnisstruktur ``` -/requirements I want to build a project management tool for small teams -where users can create projects, assign tasks, and track progress. +/etc/archivmail/ +├── config.yml # Hauptkonfiguration +└── keyfile # AES-256-GCM Key (chmod 400) + +/var/archivmail/ +├── store/ # Mailkörper (.m, verschlüsselt) +│ └── ///xxxxx.m +└── astore/ # Anhänge dedupliziert (verschlüsselt) + └── + +/var/lib/archivmail/ +└── xapian/ # Volltext-Index + +/var/log/archivmail/ +└── audit.log # Audit-Log (JSON Lines, append-only) ``` -The skill will: -1. Ask interactive questions to clarify your vision, target users, and MVP scope -2. Create your **Product Requirements Document** (`docs/PRD.md`) -3. Break the project into individual features (Single Responsibility) -4. Create all **feature specs** (`features/PROJ-1.md`, `PROJ-2.md`, etc.) -5. Update **feature tracking** (`features/INDEX.md`) -6. Recommend which feature to build first +## Standard-Zugangsdaten -You don't need to put everything in the first prompt - a brief description is enough. The skill asks follow-up questions interactively. +> **Wichtig:** Passwörter nach dem ersten Login ändern! -### 5. Build Features +| Benutzer | Passwort | Rolle | +|----------|----------|-------| +| `admin@archivmail` | `archivmailrockz` | Admin (Konfiguration, Nutzerverwaltung) | +| `auditor@archivmail` | `archivmailrockz` | Auditor (alle E-Mails + Audit-Log) | -After project initialization, build features one at a time using skills: +### Rollenübersicht -``` -/architecture Design the tech approach for features/PROJ-1-user-auth.md -/frontend Build the UI for features/PROJ-1-user-auth.md -/backend Build the API for features/PROJ-1-user-auth.md -/qa Test features/PROJ-1-user-auth.md -/deploy Deploy to Vercel -``` +| Rolle | E-Mails (eigene) | E-Mails (alle) | Audit-Log | Konfiguration | +|-------|:-:|:-:|:-:|:-:| +| `user` | ✅ | ❌ | ❌ | ❌ | +| `auditor` | ✅ | ✅ | ✅ | ❌ | +| `admin` | ❌ | ❌ | ❌ | ✅ | -Each skill suggests the next step when it finishes. Handoffs are always user-initiated. +## Technologie -To add more features later, run `/requirements` again - it detects the existing PRD and adds a single feature. +| Komponente | Technologie | +|-----------|-------------| +| Backend | Go | +| Datenbank | PostgreSQL | +| Volltext-Index | Xapian | +| Verschlüsselung | AES-256-GCM | +| Webserver | eingebettet (Go net/http) | ---- +## Lizenz -## Available Skills - -| Skill | Command | What It Does | -|-------|---------|-------------| -| Requirements Engineer | `/requirements` | Creates feature specs with user stories, acceptance criteria, edge cases | -| Solution Architect | `/architecture` | Designs PM-friendly tech architecture (no code, only high-level design) | -| Frontend Developer | `/frontend` | Builds UI with React, Tailwind CSS, and shadcn/ui | -| Backend Developer | `/backend` | Builds APIs, database schemas, RLS policies with Supabase | -| QA Engineer | `/qa` | Tests features against acceptance criteria + security audit | -| DevOps | `/deploy` | Deploys to Vercel with production-ready checks | -| Help | `/help` | Context-aware guide: shows where you are and what to do next | - -### How Skills Work - -- **Skills** are defined in `.claude/skills/` and auto-discovered by Claude Code -- **Rules** in `.claude/rules/` are auto-applied based on file context (no manual loading) -- **Sub-Agents** run heavy tasks (frontend, backend, QA) in isolated contexts for cost efficiency -- **CLAUDE.md** provides project context automatically at every session start - ---- - -## Development Workflow - -``` -1. Define /requirements --> Feature spec in features/PROJ-X.md -2. Design /architecture --> Tech design added to feature spec -3. Build /frontend --> UI components implemented - /backend --> APIs + database (if needed) -4. Test /qa --> Test results added to feature spec -5. Ship /deploy --> Deployed to Vercel -``` - -### Feature Tracking - -Features are tracked in `features/INDEX.md`: - -| ID | Feature | Status | Spec | -|----|---------|--------|------| -| PROJ-1 | User Login | Deployed | [Spec](features/PROJ-1-user-login.md) | -| PROJ-2 | Dashboard | In Progress | [Spec](features/PROJ-2-dashboard.md) | - -Every skill reads this file at start and updates it when done, preventing duplicate work. - ---- - -## Tech Stack - -| Category | Tool | Why? | -|----------|------|------| -| **Framework** | Next.js 16 | React + Server Components + App Router | -| **Language** | TypeScript | Type safety | -| **Styling** | Tailwind CSS | Utility-first CSS | -| **UI Library** | shadcn/ui | Copy-paste, customizable components | -| **Backend** | Supabase (optional) | PostgreSQL + Auth + Storage + Realtime | -| **Deployment** | Vercel | Zero-config Next.js hosting | -| **Validation** | Zod | Runtime type validation | - ---- - -## Project Structure - -``` -ai-coding-starter-kit/ -+-- CLAUDE.md <-- Auto-loaded project context -+-- .claude/ -| +-- settings.json <-- Team permissions (committed) -| +-- settings.local.json <-- Personal overrides (gitignored) -| +-- rules/ <-- Auto-applied coding rules -| | +-- general.md Git workflow, feature tracking -| | +-- frontend.md shadcn/ui, component standards -| | +-- backend.md RLS, validation, queries -| | +-- security.md Secrets, headers, auth -| +-- skills/ <-- Invocable workflows (/command) -| | +-- requirements/SKILL.md /requirements -| | +-- architecture/SKILL.md /architecture -| | +-- frontend/SKILL.md /frontend (runs as sub-agent) -| | +-- backend/SKILL.md /backend (runs as sub-agent) -| | +-- qa/SKILL.md /qa (runs as sub-agent) -| | +-- deploy/SKILL.md /deploy -| | +-- help/SKILL.md /help -| +-- agents/ <-- Sub-agent configs -| +-- frontend-dev.md Model, tools, limits -| +-- backend-dev.md -| +-- qa-engineer.md -+-- features/ <-- Feature specifications -| +-- INDEX.md Status tracking -| +-- README.md Spec format documentation -+-- docs/ -| +-- PRD.md <-- Product Requirements Document -| +-- production/ <-- Production setup guides -| +-- error-tracking.md Sentry setup (5 min) -| +-- security-headers.md XSS/Clickjacking protection -| +-- performance.md Lighthouse, optimization -| +-- database-optimization.md Indexing, N+1, caching -| +-- rate-limiting.md Upstash Redis -+-- src/ -| +-- app/ <-- Pages (Next.js App Router) -| +-- components/ -| | +-- ui/ <-- shadcn/ui components (35+ installed) -| +-- hooks/ <-- Custom React hooks -| +-- lib/ <-- Utilities -+-- public/ <-- Static files -``` - ---- - -## Getting Started - -### 1. Fill Out Your PRD - -Define your product vision in `docs/PRD.md`: -- What are you building and why? -- Who are the target users? -- What features are on the roadmap? - -### 2. Build Your First Feature - -Run `/requirements` with your feature idea. The skill will: -- Ask interactive questions to clarify requirements -- Create a feature spec in `features/PROJ-1-name.md` -- Update `features/INDEX.md` with the new feature -- Suggest running `/architecture` as the next step - -### 3. Add shadcn/ui Components (as needed) - -35+ components are pre-installed. Add more as needed: -```bash -npx shadcn@latest add [component-name] -``` - -### 4. Production Setup (first deployment) - -When you're ready to deploy, the `/deploy` skill guides you through: -- Vercel setup and deployment -- Error tracking with Sentry -- Security headers configuration -- Performance monitoring with Lighthouse - -See `docs/production/` for detailed setup guides. - ---- - -## How It Works Under the Hood - -### Skills (`.claude/skills/`) -Each skill is a structured workflow that Claude Code discovers automatically. Skills can run inline (in the main conversation) or as forked sub-agents (isolated context window). - -| Skill | Execution | Why? | -|-------|-----------|------| -| `/requirements` | Inline | Needs live interaction with user | -| `/architecture` | Inline | Short output, user reviews in real-time | -| `/frontend` | Sub-agent (forked) | Heavy file editing, lots of output | -| `/backend` | Sub-agent (forked) | Heavy file editing, SQL, API code | -| `/qa` | Sub-agent (forked) | Systematic testing, lots of output | -| `/deploy` | Inline | Deployment needs user oversight | -| `/help` | Inline | Quick status check and guidance | - -### Rules (`.claude/rules/`) -Coding standards that are auto-applied based on which files Claude is working with. No manual loading needed. - -### Sub-Agent Configs (`.claude/agents/`) -Lightweight configurations that define model, tool access, and turn limits for forked skills. - -### CLAUDE.md -Auto-loaded at every session start. Contains tech stack, conventions, and references to PRD and feature index. - ---- - -## Context Engineering - -AI agents work best with clean, structured context - not longer prompts. This template is designed around these principles: - -### State lives in files, not in memory - -Every skill reads `features/INDEX.md` and the relevant feature spec at start. After context compaction or a new session, nothing is lost - the agent simply re-reads the files. Progress tracking, acceptance criteria, and tech designs all live in markdown files, not in the conversation. - -### Context is layered - -Not everything is loaded at once. Information is layered by relevance: - -| Layer | What | When loaded | -|-------|------|-------------| -| `CLAUDE.md` | Tech stack, conventions, commands | Every session (auto) | -| `.claude/rules/` | Coding standards | When editing matching files (auto) | -| Skill `SKILL.md` | Workflow instructions | When skill is invoked | -| Feature spec | Requirements, AC, tech design | On demand (skill reads it) | -| `docs/production/` | Deployment guides | Only when referenced | - -### Context is isolated - -Heavy implementation skills (`/frontend`, `/backend`, `/qa`) run as **forked sub-agents** with their own context window. Research noise from one skill doesn't pollute another. Each fork starts clean and loads only what it needs. - -### Context recovery is built in - -All forked skills include a **Context Recovery** section: if the context is compacted mid-task, the agent re-reads the feature spec, checks `git diff` for progress, and continues without restarting or duplicating work. - -### Always read, never guess - -A global rule (`rules/general.md`) enforces: always read a file before modifying it, never assume contents from memory, verify import paths and API routes by reading. This prevents hallucinated code references - the most common source of AI coding errors. - ---- - -## Customization for Your Team - -This template is designed as a starting point. Customize it for your team: - -1. **Edit CLAUDE.md** - Add your project-specific conventions and build commands -2. **Edit docs/PRD.md** - Define your product vision and roadmap -3. **Edit .claude/rules/** - Adjust coding standards for your team -4. **Edit .claude/skills/** - Modify workflows to match your process -5. **Edit .claude/settings.json** - Configure team permissions - ---- - -## Production Guides - -Standalone guides in `docs/production/`: - -| Guide | Setup Time | What It Does | -|-------|-----------|-------------| -| [Error Tracking](docs/production/error-tracking.md) | 5 min | Sentry integration for automatic error capture | -| [Security Headers](docs/production/security-headers.md) | 2 min | XSS, Clickjacking, MIME sniffing protection | -| [Performance](docs/production/performance.md) | 10 min | Lighthouse checks, image optimization, caching | -| [Database Optimization](docs/production/database-optimization.md) | 15 min | Indexing, N+1 prevention, query optimization | -| [Rate Limiting](docs/production/rate-limiting.md) | 10 min | Upstash Redis for API abuse prevention | - ---- - -## Scripts - -```bash -npm run dev # Development server (localhost:3000) -npm run build # Production build -npm run start # Production server -npm run lint # ESLint -``` - ---- - -## Author - -Created by **Alex Sprogis** – AI Product Engineer & Content Creator. - -- [YouTube](https://www.youtube.com/@alex.sprogis) -- [Website](https://alexsprogis.de) - ---- - -## License - -MIT License - feel free to use for your projects! +Proprietär diff --git a/cmd/archivmail-export/main.go b/cmd/archivmail-export/main.go new file mode 100644 index 0000000..89bb048 --- /dev/null +++ b/cmd/archivmail-export/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "flag" + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/archivmail/config" + "github.com/archivmail/internal/index" + "github.com/archivmail/internal/storage" +) + +func main() { + configPath := flag.String("config", "/etc/archivmail/config.yml", "path to config file") + format := flag.String("format", "eml", "export format: eml or pdf") + outDir := flag.String("out", "./export", "output directory") + flag.Parse() + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + + if *format == "pdf" { + fmt.Fprintln(os.Stdout, "PDF export not yet implemented") + os.Exit(0) + } + + if *format != "eml" { + fmt.Fprintf(os.Stderr, "unknown format: %s (supported: eml, pdf)\n", *format) + os.Exit(1) + } + + cfg, err := config.Load(*configPath) + if err != nil { + logger.Error("failed to load config", "err", err) + os.Exit(1) + } + + mailStore, err := storage.New(cfg.Storage.StorePath) + if err != nil { + logger.Error("storage init failed", "err", err) + os.Exit(1) + } + + 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() + + if err := os.MkdirAll(*outDir, 0o755); err != nil { + logger.Error("cannot create output directory", "dir", *outDir, "err", err) + os.Exit(1) + } + + // Fetch all indexed mails using pagination + page := 0 + pageSize := 500 + exported := 0 + errors := 0 + + for { + result, err := idx.Search(index.SearchRequest{ + PageSize: pageSize, + Page: page, + }) + if err != nil { + logger.Error("search failed", "err", err) + os.Exit(1) + } + + if len(result.Hits) == 0 { + break + } + + for _, hit := range result.Hits { + raw, err := mailStore.Load(hit.ID) + if err != nil { + logger.Error("load failed", "id", hit.ID, "err", err) + errors++ + continue + } + + outPath := filepath.Join(*outDir, hit.ID+".eml") + if err := os.WriteFile(outPath, raw, 0o644); err != nil { + logger.Error("write failed", "path", outPath, "err", err) + errors++ + continue + } + + exported++ + } + + logger.Info("export progress", "page", page, "exported", exported, "errors", errors) + + if exported+errors >= result.Total { + break + } + page++ + } + + logger.Info("export complete", + "format", *format, + "out", *outDir, + "exported", exported, + "errors", errors, + ) +} diff --git a/cmd/archivmail-import/main.go b/cmd/archivmail-import/main.go new file mode 100644 index 0000000..d930a5f --- /dev/null +++ b/cmd/archivmail-import/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "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 ") + 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(cfg.Storage.StorePath) + if err != nil { + logger.Error("storage init failed", "err", err) + os.Exit(1) + } + + 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(raw, pm.Date) + 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, + ) +} diff --git a/cmd/archivmail/main.go b/cmd/archivmail/main.go new file mode 100644 index 0000000..a1a5618 --- /dev/null +++ b/cmd/archivmail/main.go @@ -0,0 +1,167 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/archivmail/config" + "github.com/archivmail/internal/api" + "github.com/archivmail/internal/audit" + "github.com/archivmail/internal/auth" + imapstore "github.com/archivmail/internal/imap" + "github.com/archivmail/internal/index" + "github.com/archivmail/internal/smtpd" + "github.com/archivmail/internal/storage" + "github.com/archivmail/internal/userstore" +) + +func main() { + configPath := flag.String("config", "/etc/archivmail/config.yml", "path to config file") + flag.Parse() + + 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", "path", *configPath, "err", err) + os.Exit(1) + } + + // Storage + mailStore, err := storage.New(cfg.Storage.StorePath) + if err != nil { + logger.Error("storage init failed", "err", err) + os.Exit(1) + } + + // Index + 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() + + // User store + users, err := userstore.New(cfg.Database.DSN()) + if err != nil { + logger.Error("userstore init failed", "err", err) + os.Exit(1) + } + defer users.Close() + + // Audit log + audlog, err := audit.New(cfg.Database.DSN(), cfg.Audit.LogPath, logger) + if err != nil { + logger.Error("audit init failed", "err", err) + os.Exit(1) + } + defer audlog.Close() + + // Seed default users on first run + if err := seedDefaultUsers(users, logger); err != nil { + logger.Error("seed users failed", "err", err) + } + + // Auth manager + authMgr := auth.New(users, nil, cfg.API.Secret) + + // API server + apiCfg := config.APIConfig{ + Bind: cfg.API.Bind, + Secret: cfg.API.Secret, + } + srv := api.New(apiCfg, mailStore, idx, authMgr, users, audlog, logger) + + bind := cfg.API.Bind + if bind == "" { + bind = fmt.Sprintf(":%d", cfg.Server.APIPort) + } + + httpServer := &http.Server{ + Addr: bind, + Handler: srv, + } + + // Start SMTP daemon + if cfg.SMTP.Bind == "" { + cfg.SMTP.Bind = fmt.Sprintf(":%d", cfg.Server.SMTPPort) + } + smtpDaemon := smtpd.New(cfg.SMTP, mailStore, logger) + if err := smtpDaemon.Start(); err != nil { + logger.Error("SMTP daemon failed to start", "err", err) + os.Exit(1) + } + defer smtpDaemon.Stop() + + // Wire SMTP daemon into API server for status endpoint + srv.SetSMTPDaemon(smtpDaemon) + + // IMAP store + importer + imapSt, err := imapstore.New(cfg.Database.DSN(), cfg.API.Secret) + if err != nil { + logger.Error("imap store init failed", "err", err) + os.Exit(1) + } + defer imapSt.Close() + imapImp := imapstore.NewImporter(imapSt, mailStore, idx, logger) + srv.SetImap(imapSt, imapImp) + + // Start HTTP API + go func() { + logger.Info("starting API server", "addr", bind) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error("API server error", "err", err) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logger.Info("shutting down...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + httpServer.Shutdown(ctx) +} + +// seedDefaultUsers creates default admin and auditor accounts if no users exist yet. +func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error { + all, err := users.List("") + if err != nil { + return fmt.Errorf("list users: %w", err) + } + if len(all) > 0 { + return nil // already seeded + } + defaults := []userstore.CreateUserRequest{ + {Username: "admin", Email: "admin@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAdmin}, + {Username: "auditor", Email: "auditor@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAuditor}, + } + for _, req := range defaults { + if _, err := users.Create(req); err != nil { + return fmt.Errorf("create default user %s: %w", req.Username, err) + } + logger.Info("created default user", "username", req.Username, "role", req.Role) + } + logger.Warn("default users created — change passwords immediately!", "admin", "admin", "auditor", "auditor") + return nil +} + + diff --git a/config.test.yml b/config.test.yml new file mode 100644 index 0000000..535bff1 --- /dev/null +++ b/config.test.yml @@ -0,0 +1,35 @@ +database: + host: 127.0.0.1 + port: 5432 + name: archivmail_test + user: archivmail + password: testpass + sslmode: disable + +storage: + path: /tmp/archivmail-test/store + max_size_mb: 100 + +smtp: + enabled: true + bind: ":2525" + domain: "localhost" + tls_cert: "" + tls_key: "" + max_size_mb: 10 + +imap: [] + +api: + bind: ":8080" + secret: "dev-secret-change-in-production-min32" + tls: false + +index: + path: /tmp/archivmail-test/xapian + backend: xapian + batch_size: 10 + +logging: + path: /tmp/archivmail-test/logs + level: debug diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f512f4e --- /dev/null +++ b/config/config.go @@ -0,0 +1,100 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// APIConfig holds configuration for the HTTP API server. +type APIConfig struct { + Bind string `yaml:"bind"` + Secret string `yaml:"secret"` +} + +// Config is the full application configuration loaded from YAML. +type Config struct { + Server ServerConfig `yaml:"server"` + Storage StorageConfig `yaml:"storage"` + Database DatabaseConfig `yaml:"database"` + SMTP SMTPConfig `yaml:"smtp"` + API APIConfig `yaml:"api"` + Index IndexConfig `yaml:"index"` + Audit AuditConfig `yaml:"audit"` + Logging LoggingConfig `yaml:"logging"` +} + +// ServerConfig holds port settings for the main services. +type ServerConfig struct { + APIPort int `yaml:"api_port"` + SMTPPort int `yaml:"smtp_port"` +} + +// StorageConfig holds file system paths for email storage. +type StorageConfig struct { + StorePath string `yaml:"store_path"` + AStorePath string `yaml:"astore_path"` + XapianPath string `yaml:"xapian_path"` + Keyfile string `yaml:"keyfile"` +} + +// DatabaseConfig holds PostgreSQL connection settings. +type DatabaseConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Name string `yaml:"name"` + User string `yaml:"user"` + Password string `yaml:"password"` + SSLMode string `yaml:"sslmode"` +} + +// DSN builds a PostgreSQL connection string from the config fields. +func (d DatabaseConfig) DSN() string { + return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + d.User, d.Password, d.Host, d.Port, d.Name, d.SSLMode) +} + +// SMTPConfig holds settings for the embedded SMTP server. +type SMTPConfig struct { + Enabled bool `yaml:"enabled"` + Bind string `yaml:"bind"` + Domain string `yaml:"domain"` + TLSCert string `yaml:"tls_cert"` + TLSKey string `yaml:"tls_key"` + MaxSizeMB int `yaml:"max_size_mb"` + AllowedIPs []string `yaml:"allowed_ips"` +} + +// IndexConfig holds full-text index settings. +type IndexConfig struct { + Path string `yaml:"path"` + Backend string `yaml:"backend"` + BatchSize int `yaml:"batch_size"` + AsyncQueueSize int `yaml:"async_queue_size"` +} + +// AuditConfig holds audit log settings. +type AuditConfig struct { + LogPath string `yaml:"log_path"` + RetentionDays int `yaml:"retention_days"` +} + +// LoggingConfig holds application logging settings. +type LoggingConfig struct { + Path string `yaml:"path"` + Level string `yaml:"level"` +} + +// Load reads a YAML config file from path and returns a parsed Config. +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/docs/PRD.md b/docs/PRD.md index 7c4e95f..21ef6a7 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,29 +1,57 @@ # Product Requirements Document ## Vision -_Describe what you are building and why._ +Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System, das E-Mails aus verschiedenen Quellen (IMAP, SMTP, EML/MBOX) importiert, volltext-indexiert und DSGVO-konform langzeitarchiviert. Admins verwalten Nutzer und Postfächer; alle Mitarbeiter können gezielt im Archiv suchen. ## Target Users -_Who will use this product? Describe their needs and pain points._ + +**Primär: Unternehmen (5–500 Mitarbeiter)** +- **Endnutzer:** Mitarbeiter, die E-Mails im Archiv suchen und lesen wollen +- **Admins:** IT-Verantwortliche, die Nutzer, Postfächer und Import-Quellen verwalten +- **Compliance-Verantwortliche:** Prüfen Audit-Logs, exportieren E-Mails für Anfragen + +**Pain Points:** +- E-Mail-Clients bieten keine revisionssichere Langzeitarchivierung +- Suche über mehrere Konten und Postfächer ist unzuverlässig +- Datenschutzbedenken bei Cloud-basierten Archivlösungen +- Nachweis der Unverändertheit von E-Mails für Compliance erforderlich ## Core Features (Roadmap) | Priority | Feature | Status | |----------|---------|--------| -| P0 (MVP) | _Feature 1_ | Planned | -| P0 (MVP) | _Feature 2_ | Planned | -| P1 | _Feature 3_ | Planned | -| P2 | _Feature 4_ | Planned | +| P0 (MVP) | Nutzer-Authentifizierung & Rollen (User/Admin) | Planned | +| P0 (MVP) | E-Mail-Import: SMTP-Eingang via BCC *(primärer Eingangsweg)* | Planned | +| P1 | E-Mail-Import: IMAP-Verbindung *(sekundär, Altbestände)* | Planned | +| P1 | E-Mail-Import: POP3-Verbindung *(sekundär, Altbestände)* | Planned | +| P1 | E-Mail-Import: EML/MBOX Upload *(sekundär, Migration)* | Planned | +| P0 (MVP) | E-Mail-Speicherung & Volltext-Indexierung | Planned | +| P0 (MVP) | Volltext-Suche & Filterung | Planned | +| P0 (MVP) | E-Mail-Ansicht (Lesen & Anhänge) | Planned | +| P1 | Automatischer IMAP-Sync (Cron-Job) | Planned | +| P1 | Ordner- & Label-Verwaltung | Planned | +| P1 | Admin-Bereich: Nutzer- & Postfachverwaltung | Planned | +| P2 | Audit-Log & Compliance-Berichte | Planned | +| P2 | E-Mail-Export (EML/PDF) | Planned | +| P2 | REST API für externe CRM-Anbindung | Planned | ## Success Metrics -_How will you measure success? (e.g., user signups, retention, task completion rate)_ +- Import und Indexierung von 100.000+ E-Mails ohne Performanceprobleme +- Volltext-Suchanfragen liefern Ergebnisse in < 2 Sekunden +- SMTP-Journaling empfängt E-Mails in Echtzeit (< 5 Sekunden Verzögerung) +- Automatischer IMAP-Sync läuft stabil über Monate ohne manuelle Eingriffe +- Audit-Log erfasst alle Zugriffe lückenlos ## Constraints -_Budget, timeline, technical limitations, team size._ +- **Self-hosted:** Kein Zwang zu externen Cloud-Diensten – läuft on-premise oder auf eigenem Server +- **DSGVO-konform:** E-Mails verlassen nie den eigenen Server; Löschkonzept vorhanden +- **Großes Volumen:** Architektur skaliert auf > 100.000 E-Mails +- **Einfache Architektur:** Go-Backend REST API + Next.js-Frontend, kleines Team (1–2 Devs) +- **Tech Stack:** Go (Backend, SMTP-Daemon, Storage, Xapian) + Next.js/TypeScript (Web-GUI) + PostgreSQL ## Non-Goals -_What are you explicitly NOT building in this version?_ - ---- - -Use `/requirements` to create detailed feature specifications for each item in the roadmap above. +- Kein vollständiger E-Mail-Client (kein eigenständiges Senden von E-Mails durch Nutzer) +- Keine Ende-zu-Ende-Verschlüsselung im MVP +- Kein Enterprise-LDAP/SSO im MVP (nur lokale Accounts) +- Keine mobile App (nur Web-Interface) +- Keine automatische GoBD/SOX-Zertifizierung im MVP diff --git a/docs/api-v1.md b/docs/api-v1.md new file mode 100644 index 0000000..1a84142 --- /dev/null +++ b/docs/api-v1.md @@ -0,0 +1,252 @@ +# archivmail REST API v1 + +> **Lese-API** – alle Endpunkte sind read-only (`GET`). Schreiboperationen sind nicht verfügbar. + +## Authentifizierung + +Jede Anfrage muss einen gültigen API-Key im HTTP-Header mitschicken: + +``` +Authorization: Bearer +``` + +API-Keys werden vom Admin im Admin-Bereich generiert und verwaltet. Ein API-Key hat eine zugewiesene Rolle (`user` oder `auditor`), die den Zugriffsumfang bestimmt. + +| Rolle | Zugriff | +|-------|---------| +| `user` | Nur E-Mails aus zugewiesenen Postfächern | +| `auditor` | Alle E-Mails (postfachübergreifend) | + +--- + +## Fehlercodes + +| Code | Bedeutung | +|------|-----------| +| `200 OK` | Erfolg | +| `400 Bad Request` | Ungültige Parameter | +| `401 Unauthorized` | API-Key fehlt, ungültig oder deaktiviert | +| `403 Forbidden` | API-Key hat keine Berechtigung für diese Ressource | +| `404 Not Found` | E-Mail nicht gefunden | +| `405 Method Not Allowed` | Schreibmethode (POST, PUT, DELETE etc.) nicht erlaubt | +| `429 Too Many Requests` | Rate-Limit überschritten (Standard: 60 Anfragen/Minute) | +| `500 Internal Server Error` | Serverfehler | + +--- + +## Endpunkte + +--- + +### E-Mails suchen / filtern + +``` +GET /api/v1/mails +``` + +**Query-Parameter (alle optional, kombinierbar):** + +| Parameter | Typ | Beschreibung | Beispiel | +|-----------|-----|-------------|---------| +| `q` | string | Volltext-Suche (Xapian QueryParser) | `q=Rechnung+2024` | +| `from` | string | Absender (exakt oder Partial-Match) | `from=alice@firma.de` | +| `to` | string | Empfänger | `to=bob@firma.de` | +| `cc` | string | CC-Empfänger | `cc=team@firma.de` | +| `contact` | string | From **oder** To **oder** CC enthält diese Adresse | `contact=kunde@example.com` | +| `subject` | string | Betreff enthält diesen Text | `subject=Angebot` | +| `date_from` | string (ISO 8601) | Mails ab diesem Datum | `date_from=2024-01-01` | +| `date_to` | string (ISO 8601) | Mails bis zu diesem Datum | `date_to=2024-12-31` | +| `has_attachments` | boolean | Nur Mails mit/ohne Anhänge | `has_attachments=true` | +| `page` | integer | Seite (Standard: 1) | `page=2` | +| `limit` | integer | Ergebnisse pro Seite (Standard: 25, max: 100) | `limit=50` | +| `sort` | string | Sortierung: `date_asc`, `date_desc` (Standard), `relevance` | `sort=date_asc` | + +**Beispiel-Request:** +``` +GET /api/v1/mails?contact=kunde@example.com&date_from=2024-01-01&limit=25 +Authorization: Bearer am_abc123... +``` + +**Antwort:** +```json +{ + "total": 142, + "page": 1, + "limit": 25, + "pages": 6, + "mails": [ + { + "message_id": "", + "from": "alice@firma.de", + "to": ["kunde@example.com"], + "cc": [], + "subject": "Angebot vom 15.03.2024", + "date": "2024-03-15T10:23:00Z", + "size": 24680, + "has_attachments": true, + "attachments": [ + { + "filename": "Angebot_2024.pdf", + "mime_type": "application/pdf", + "size": 18432 + } + ] + } + ] +} +``` + +--- + +### Einzelne E-Mail abrufen (Metadaten + Body) + +``` +GET /api/v1/mails/{message_id} +``` + +**Pfad-Parameter:** + +| Parameter | Beschreibung | +|-----------|-------------| +| `message_id` | RFC-2822 Message-ID (URL-encoded) | + +**Beispiel-Request:** +``` +GET /api/v1/mails/%3Cabc123%40mailserver.firma.de%3E +Authorization: Bearer am_abc123... +``` + +**Antwort:** +```json +{ + "message_id": "", + "from": "alice@firma.de", + "to": ["kunde@example.com"], + "cc": [], + "subject": "Angebot vom 15.03.2024", + "date": "2024-03-15T10:23:00Z", + "size": 24680, + "body_plain": "Sehr geehrte Damen und Herren,\n\nim Anhang finden Sie...", + "body_html": "...", + "has_attachments": true, + "attachments": [ + { + "filename": "Angebot_2024.pdf", + "mime_type": "application/pdf", + "size": 18432, + "download_url": "/api/v1/mails/%3Cabc123%40mailserver.firma.de%3E/attachments/0" + } + ] +} +``` + +--- + +### Original-E-Mail als EML herunterladen + +``` +GET /api/v1/mails/{message_id}/raw +``` + +Gibt die originale, unveränderte E-Mail im RFC-2822-Format zurück. + +**Antwort-Header:** +``` +Content-Type: message/rfc822 +Content-Disposition: attachment; filename=".eml" +``` + +--- + +### Einzelnen Anhang herunterladen + +``` +GET /api/v1/mails/{message_id}/attachments/{index} +``` + +**Pfad-Parameter:** + +| Parameter | Beschreibung | +|-----------|-------------| +| `message_id` | RFC-2822 Message-ID (URL-encoded) | +| `index` | Position des Anhangs (0-basiert, aus der Attachment-Liste) | + +**Antwort-Header:** +``` +Content-Type: application/pdf (je nach MIME-Type des Anhangs) +Content-Disposition: attachment; filename="Angebot_2024.pdf" +``` + +--- + +### Server-Info + +``` +GET /api/v1/info +``` + +Gibt Version und Status des Servers zurück. Nützlich zum Testen der Verbindung. + +**Antwort:** +```json +{ + "version": "1.0.0", + "status": "ok", + "mail_count": 142857 +} +``` + +--- + +## Volltext-Suche Syntax (`q`-Parameter) + +Der `q`-Parameter unterstützt Xapian QueryParser-Syntax: + +| Syntax | Beispiel | Bedeutung | +|--------|---------|-----------| +| Einfacher Begriff | `Rechnung` | Enthält "Rechnung" | +| Mehrere Begriffe | `Rechnung Mahnung` | Enthält beide Begriffe (AND) | +| Phrasensuche | `"offene Rechnung"` | Exakter Ausdruck | +| OR-Verknüpfung | `Rechnung OR Angebot` | Eines von beiden | +| Ausschließen | `Rechnung NOT Storno` | Rechnung aber nicht Storno | +| Wildcard | `Rechnun*` | Beginnt mit "Rechnun" | +| Feldspezifisch | `subject:Angebot` | Nur im Betreff suchen | +| Feldspezifisch | `from:alice@firma.de` | Nur von diesem Absender | + +--- + +## Paginierung + +Alle Listen-Endpunkte sind paginiert: + +``` +GET /api/v1/mails?page=2&limit=50 +``` + +Die Antwort enthält immer: +- `total` – Gesamtanzahl der Treffer +- `page` – aktuelle Seite +- `limit` – Einträge pro Seite +- `pages` – Gesamtanzahl der Seiten + +Maximum: 100 Einträge pro Anfrage. Für größere Abfragen mehrere Seiten iterieren. + +--- + +## Rate Limiting + +Standard: **60 Anfragen pro Minute** pro API-Key (konfigurierbar durch Admin). + +Bei Überschreitung: +``` +HTTP 429 Too Many Requests +Retry-After: 30 +``` + +--- + +## Changelog + +| Version | Datum | Änderungen | +|---------|-------|-----------| +| v1.0 | 2026-03-13 | Initiale Version | diff --git a/features/INDEX.md b/features/INDEX.md index 89f11d1..6e12293 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -12,7 +12,25 @@ | ID | Feature | Status | Spec | Created | |----|---------|--------|------|---------| +| PROJ-1 | Nutzer-Authentifizierung & Rollen (User/Admin) | In Progress | [PROJ-1](PROJ-1-authentifizierung-und-rollen.md) | 2026-03-12 | +| PROJ-2 | E-Mail-Import: EML/MBOX Upload | In Progress | [PROJ-2](PROJ-2-import-eml-mbox.md) | 2026-03-12 | +| PROJ-3 | E-Mail-Import: IMAP-Verbindung | In Progress | [PROJ-3](PROJ-3-import-imap.md) | 2026-03-12 | +| PROJ-4 | E-Mail-Import: SMTP-Eingang via BCC (primär) | In Progress | [PROJ-4](PROJ-4-import-smtp.md) | 2026-03-12 | +| PROJ-5 | E-Mail-Speicherung & Volltext-Indexierung | In Progress | [PROJ-5](PROJ-5-speicherung-und-indexierung.md) | 2026-03-12 | +| PROJ-6 | Volltext-Suche & Filterung | In Progress | [PROJ-6](PROJ-6-volltext-suche.md) | 2026-03-12 | +| PROJ-7 | E-Mail-Ansicht (Lesen & Anhänge) | In Progress | [PROJ-7](PROJ-7-email-ansicht.md) | 2026-03-12 | +| PROJ-8 | Automatischer IMAP-Sync (Cron-Job) | In Progress | [PROJ-8](PROJ-8-imap-auto-sync.md) | 2026-03-12 | +| PROJ-9 | Ordner- & Label-Verwaltung | In Progress | [PROJ-9](PROJ-9-ordner-und-labels.md) | 2026-03-12 | +| PROJ-10 | Admin-Bereich: Nutzer- & Postfachverwaltung | In Progress | [PROJ-10](PROJ-10-admin-bereich.md) | 2026-03-12 | +| PROJ-11 | Audit-Log & Compliance-Berichte | In Progress | [PROJ-11](PROJ-11-audit-log.md) | 2026-03-12 | +| PROJ-12 | E-Mail-Export (EML/PDF) | In Progress | [PROJ-12](PROJ-12-export.md) | 2026-03-12 | +| PROJ-13 | REST API für externe CRM-Anbindung | In Progress | [PROJ-13](PROJ-13-rest-api-crm.md) | 2026-03-13 | +| PROJ-14 | E-Mail-Import: POP3-Verbindung | In Progress | [PROJ-14](PROJ-14-import-pop3.md) | 2026-03-13 | +| PROJ-15 | CLI Import & Export (archivmail-User) | In Progress | [PROJ-15](PROJ-15-cli-import-export.md) | 2026-03-13 | +| PROJ-16 | LDAP / Active Directory Anbindung | In Progress | [PROJ-16](PROJ-16-ldap-active-directory.md) | 2026-03-13 | + +| PROJ-17 | Admin Dashboard – Systemauslastung & Archiv-Übersicht | In Review | [PROJ-17](PROJ-17-system-dashboard.md) | 2026-03-14 | -## Next Available ID: PROJ-1 +## Next Available ID: PROJ-18 diff --git a/features/PROJ-1-authentifizierung-und-rollen.md b/features/PROJ-1-authentifizierung-und-rollen.md new file mode 100644 index 0000000..fca90e4 --- /dev/null +++ b/features/PROJ-1-authentifizierung-und-rollen.md @@ -0,0 +1,185 @@ +# PROJ-1: Nutzer-Authentifizierung & Rollen + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- PROJ-16 (LDAP / Active Directory Anbindung) — optionale Erweiterung des Login-Flows + +## Rollen-Übersicht + +| Rolle | Zugriff | +|-------|---------| +| `user` | Suche und Lesen eigener archivierter E-Mails | +| `auditor` | Alle E-Mails lesen und suchen (postfachübergreifend) + Audit-Log einsehen und exportieren – keine Konfiguration | +| `admin` | Konfiguration, Nutzerverwaltung, Import-Quellen, Systemeinstellungen – kein Zugriff auf E-Mails, kein Audit-Log | + +## User Stories +- Als Admin möchte ich mich mit Benutzername/Passwort einloggen, damit nur autorisierte Personen Zugriff haben. +- Als Admin möchte ich neue Nutzer anlegen und ihnen eine Rolle zuweisen (`user`, `auditor`, `admin`). +- Als Auditor möchte ich den Audit-Log einsehen und als CSV exportieren, ohne Zugriff auf E-Mails oder Konfiguration zu haben. +- Als Nutzer möchte ich mich abmelden können, damit meine Session sicher beendet wird. +- Als Admin möchte ich Passwörter zurücksetzen können, damit gesperrte Nutzer wieder Zugang erhalten. +- Als System möchte ich Sessions nach Inaktivität automatisch beenden, damit unbefugter Zugriff verhindert wird. + +## Acceptance Criteria +- [ ] Login-Seite mit E-Mail/Benutzername und Passwort-Formular +- [ ] Fehlermeldung bei falschen Zugangsdaten (kein Hinweis ob E-Mail oder Passwort falsch) +- [ ] Session-Token wird sicher gespeichert (httpOnly Cookie oder JWT) +- [ ] Sessions laufen nach konfigurierbarer Inaktivität ab (Standard: 8 Stunden) +- [ ] Drei Rollen: `user`, `auditor`, `admin` – strikt getrennte Zugriffsrechte +- [ ] `auditor` hat Zugriff auf alle E-Mails (postfachübergreifend, auch fremde Postfächer) + Audit-Log – keine Konfiguration +- [ ] `admin` hat ausschließlich Zugriff auf Konfiguration und Nutzerverwaltung – kein Zugriff auf E-Mails und kein Zugriff auf Audit-Log +- [ ] Admin kann Nutzer anlegen, deaktivieren und löschen +- [ ] Admin kann Passwörter zurücksetzen (temporäres Passwort) +- [ ] Alle API-Endpunkte prüfen Authentifizierung und Rolle +- [ ] Logout löscht die Session serverseitig + +## Edge Cases +- Login mit deaktiviertem Account → klare Fehlermeldung, kein Zugang +- Mehrfaches Fehllogin → Rate-Limiting oder Account-Sperre nach X Versuchen +- Session-Token abgelaufen → automatische Weiterleitung zur Login-Seite +- Erster Start: Zwei feste Default-User werden beim ersten Start automatisch angelegt: + - `admin@archivmail` / `archivmailrockz` (Rolle: `admin`) + - `auditor@archivmail` / `archivmailrockz` (Rolle: `auditor`) + - Passwörter sollten nach dem ersten Login geändert werden (Hinweis in der UI) +- Admin löscht sich selbst → verhindern wenn letzter Admin + +## Technical Requirements +- Passwörter mit bcrypt gehasht (min. Cost 12) +- Alle Routen außer `/login` erfordern gültige Session +- Admin-Routen (`/admin/*`) nur für `admin`-Rolle +- Audit-Routen (`/audit/*`) und E-Mail-Suche/Ansicht nur für `auditor`- und `user`-Rolle +- `admin` erhält bei E-Mail-Endpunkten HTTP 403 – keine Ausnahmen +- Keine Rolle vereint `admin` + `auditor` – strikte Funktionstrennung +- Audit-Log-Eintrag bei Login, Logout, fehlgeschlagenem Login + +--- + + +## Tech Design (Solution Architect) + +### Systemübersicht: Two-Tier Architektur + +``` +Browser (Next.js App) Go REST API Backend + │ │ + │ POST /api/auth/login │ + │ {email, password} │ + │ ─────────────────────────────────► │ + │ 1. Lokaler Account? → bcrypt verify + │ 2. Nicht gefunden + LDAP aktiv? + │ → LDAP-Bind (Service Account) + │ → User-DN suchen + │ → User-Bind mit Passwort + │ → AD-Gruppen → Rolle bestimmen + │ → UpsertLDAPUser in PostgreSQL + │ 3. Session-Token erstellen + │ Session in PostgreSQL speichern + │ ◄───────────────────────────────── + │ Set-Cookie: session=TOKEN │ + │ (httpOnly, Secure, SameSite) │ + │ │ + │ GET /api/search?q=... │ + │ Cookie: session=TOKEN │ + │ ─────────────────────────────────► │ + │ Session-Middleware: Token prüfen + │ Role-Middleware: Route erlaubt? + │ ◄───────────────────────────────── + │ JSON-Antwort │ +``` + +> **LDAP ist vollständig optional.** Wenn `ldap.enabled: false` (Standard), verhält sich das System exakt wie ohne LDAP. Lokale Accounts funktionieren immer — auch wenn LDAP aktiviert ist (Fallback bei LDAP-Fehler). + +### Komponentenstruktur + +**Next.js Frontend:** +``` +src/app/ +├── /login ← Login-Seite (öffentlich) +├── /search ← Suche + E-Mail-Ansicht (user + auditor) +├── /audit ← Audit-Log (nur auditor) +└── /admin ← Admin-Bereich (nur admin) + +src/components/ +├── LoginForm ← E-Mail + Passwort, Fehlermeldungen +├── RoleGuard ← Schützt Routen clientseitig, redirect auf /login +└── PasswordChangePrompt ← Hinweis bei Default-Passwort +``` + +**Go Backend:** +``` +HTTP-Server +├── POST /api/auth/login ← Session ausstellen +├── POST /api/auth/logout ← Session löschen +├── Session Middleware ← prüft Token bei allen /api/* Routen +├── Role Middleware +│ ├── /api/admin/* → nur `admin` +│ ├── /api/audit/* → nur `auditor` +│ └── /api/* → `user` + `auditor` (admin → 403) +├── Password Manager ← bcrypt Hash + Verify +├── User Store ← PostgreSQL users-Tabelle +└── Bootstrap ← Default-User beim ersten Start +``` + +### Datenmodell + +**Tabelle `users`:** + +| Feld | Beschreibung | +|------|-------------| +| `id` | Interne ID | +| `email` | Login-E-Mail (eindeutig) | +| `password_hash` | bcrypt-Hash (Cost 12) — NULL bei LDAP-Usern | +| `role` | `user` / `auditor` / `admin` | +| `source` | `local` oder `ldap` — Herkunft des Accounts | +| `active` | Deaktivierte Nutzer können sich nicht einloggen | +| `created_at` | Erstellungszeitpunkt | +| `last_login_at` | Letzter erfolgreicher Login | + +**Tabelle `sessions`:** + +| Feld | Beschreibung | +|------|-------------| +| `token` | Zufälliger 32-Byte-Token | +| `user_id` | Referenz auf `users` | +| `expires_at` | Ablaufzeitpunkt (rollierend, +8h bei Aktivität) | +| `last_active_at` | Wird bei jeder Anfrage aktualisiert | + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Next.js Frontend + Go REST API** | Klare Trennung: Next.js rendert die UI, Go verwaltet Daten und Sicherheit. Kein Java. | +| **Session-Cookie (httpOnly)** | Next.js sendet Cookie automatisch mit – kein manuelles Token-Handling im Frontend-Code nötig | +| **Server-side Sessions (nicht JWT)** | Logout und Admin-Deaktivierung wirken sofort – JWT wäre bis Ablauf weiterhin gültig | +| **Role-Check im Go-Backend** | Sicherheits-kritische Prüfung liegt im Backend, nicht im Next.js-Client (der wäre manipulierbar) | +| **RoleGuard in Next.js zusätzlich** | Verhindert kurzes Aufblitzen falscher Seiten – rein UX, kein Sicherheits-Feature | +| **bcrypt Cost 12** | Ausreichend langsam gegen Brute-Force | +| **LDAP als optionaler Fallback** | Login versucht erst lokalen Account, dann LDAP – Reihenfolge garantiert, dass lokale Admins immer funktionieren | +| **LDAP-User in PostgreSQL gespiegelt** | Nach erstem Login landet LDAP-User in `users`-Tabelle (`source: ldap`) – einheitliche Session-Verwaltung, kein Sonder-Code | +| **AD-Gruppen → Rollen-Mapping** | Rolle wird bei jedem Login aus AD-Gruppenmitgliedschaft neu bestimmt – Rollen-Änderung in AD wirkt beim nächsten Login | + +### Abhängigkeiten + +**Go Backend:** + +| Paket | Zweck | +|---|---| +| `golang.org/x/crypto/bcrypt` | Passwort-Hashing | +| `crypto/rand` | Sichere Token-Generierung (Stdlib) | +| `github.com/go-ldap/ldap/v3` | LDAP/AD-Authentifizierung (PROJ-16) | + +**Next.js Frontend:** + +| Paket | Zweck | +|---|---| +| `react-hook-form` + `zod` | Login-Formular-Validierung (bereits im Template) | +| `shadcn/ui` | UI-Komponenten (bereits installiert) | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-10-admin-bereich.md b/features/PROJ-10-admin-bereich.md new file mode 100644 index 0000000..7b1b8d4 --- /dev/null +++ b/features/PROJ-10-admin-bereich.md @@ -0,0 +1,161 @@ +# PROJ-10: Admin-Bereich: Nutzer- & Postfachverwaltung + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung & Rollen) – nur Admins haben Zugang + +## User Stories +- Als Admin möchte ich alle Nutzer auflisten, bearbeiten, deaktivieren und löschen. +- Als Admin möchte ich Postfächer (IMAP-Verbindungen) verwalten und Nutzern zuweisen. +- Als Admin möchte ich Systemstatistiken sehen (Gesamtanzahl E-Mails, Speicher, aktive Nutzer). +- Als Admin möchte ich Import-Konfigurationen (IMAP, SMTP) verwalten. +- Als Admin möchte ich globale Systemeinstellungen konfigurieren (Sync-Intervall, max. Upload-Größe, Retention-Policy). + +## Acceptance Criteria +- [ ] Admin-Dashboard mit Übersicht: Nutzeranzahl, E-Mail-Anzahl, Speicherverbrauch +- [ ] Nutzerliste: Anzeige aller Nutzer mit Rolle, Status, letztem Login +- [ ] Nutzer anlegen / bearbeiten / deaktivieren / löschen (mit Bestätigungsdialog) +- [ ] Postfach-Verwaltung: IMAP-Verbindungen anlegen, bearbeiten, testen, löschen +- [ ] Postfach-Zuweisung: Nutzer einem oder mehreren Postfächern zuordnen +- [ ] System-Einstellungen: Sync-Intervall, SMTP-Port, max. Upload-Größe, Retention-Tage +- [ ] Alle Admin-Aktionen werden im Audit-Log erfasst + +## Edge Cases +- Admin löscht Nutzer mit archivierten E-Mails → E-Mails bleiben im Archiv, Nutzer wird anonymisiert (DSGVO) +- Letzten Admin löschen/deaktivieren → verhindern mit Fehlermeldung +- Postfach löschen mit laufendem Sync → Sync abbrechen, dann löschen + +## Technical Requirements +- Admin-Bereich unter eigenem Route-Prefix (/admin/*) +- Alle Admin-API-Endpunkte prüfen Admin-Rolle +- Änderungen an Systemeinstellungen erfordern Server-Neustart nur wenn unvermeidlich + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend (/admin/*):** +``` +/admin +├── Dashboard ← Einstiegsseite +│ ├── StatCard: Gesamtanzahl Mails +│ ├── StatCard: Speicherverbrauch (store + astore) +│ ├── StatCard: Aktive Nutzer +│ ├── StatCard: Letzter SMTP-Eingang +│ └── ImportQueue-Status (laufende Imports) +│ +├── /admin/users ← Nutzerverwaltung +│ ├── NutzerTabelle (Name, Rolle, Status, letzter Login) +│ ├── [Nutzer anlegen] Button → NutzerFormular +│ └── NutzerRow-Aktionen +│ ├── Bearbeiten (Rolle, Passwort zurücksetzen) +│ ├── Deaktivieren / Aktivieren +│ └── Löschen (Bestätigungsdialog + DSGVO-Hinweis) +│ +├── /admin/imap ← IMAP-Verbindungen (PROJ-3 + PROJ-8) +│ ├── IMAP-Verbindungsliste +│ └── Postfach-Zuweisung (Nutzer ↔ IMAP-Account) +│ +├── /admin/pop3 ← POP3-Verbindungen (PROJ-14) +│ +├── /admin/smtp ← SMTP-Daemon-Status (PROJ-4) +│ ├── Status (läuft / gestoppt), Port, TLS +│ ├── Anzahl empfangener Mails (heute / gesamt) +│ └── IP-Allowlist verwalten +│ +├── /admin/upload ← EML/MBOX Upload (PROJ-2) +│ +├── /admin/apikeys ← API-Keys (PROJ-13) +│ +├── /admin/labels ← Globale Labels + Auto-Regeln (PROJ-9) +│ +└── /admin/settings ← Systemeinstellungen + ├── max. Upload-Größe (MB) + ├── Retention-Tage (0 = unbegrenzt) + ├── Session-Timeout (Stunden) + └── SMTP-Port (Hinweis: Neustart erforderlich) +``` + +**Go Backend (/api/admin/*):** +``` +Admin-Router (alle Routen prüfen admin-Rolle) +│ +├── GET /api/admin/stats ← Dashboard-Zahlen +│ (Mail-Count, Speicher, aktive User, letzter SMTP-Eingang) +│ +├── Nutzer-Verwaltung +│ ├── GET /api/admin/users +│ ├── POST /api/admin/users ← anlegen +│ ├── PATCH /api/admin/users/{id} ← bearbeiten +│ ├── DELETE /api/admin/users/{id} ← löschen (DSGVO-Anonymisierung) +│ └── POST /api/admin/users/{id}/reset-password +│ +├── Postfach-Zuweisung +│ ├── GET /api/admin/users/{id}/mailboxes +│ ├── POST /api/admin/users/{id}/mailboxes/{account_id} +│ └── DELETE /api/admin/users/{id}/mailboxes/{account_id} +│ +└── Systemeinstellungen + ├── GET /api/admin/settings + └── PATCH /api/admin/settings +``` + +### DSGVO-Löschfluss (Nutzer löschen) + +``` +Admin klickt "Nutzer löschen" + │ + ▼ +Bestätigungsdialog: +"E-Mails bleiben im Archiv. + Nutzerdaten werden anonymisiert." + │ + ▼ +DELETE /api/admin/users/{id} + │ + ├── Ist letzter Admin? → 409 Conflict (verhindern) + │ + ├── E-Mails des Nutzers → bleiben im Archiv (immutable) + │ + ├── Audit-Log-Einträge → user_id → "anonymized" + │ IP-Adressen → gelöscht + ├── Sessions → alle gelöscht + ├── Labels des Nutzers → gelöscht + └── User-Eintrag → gelöscht +``` + +### Datenmodell + +**Tabelle `settings`** – Key-Value-Store für Systemeinstellungen: + +| Key | Standardwert | Beschreibung | +|-----|-------------|-------------| +| `max_upload_mb` | `500` | Max. Upload-Größe in MB | +| `retention_days` | `0` | 0 = unbegrenzt | +| `session_timeout_hours` | `8` | Session-Inaktivitäts-Timeout | +| `smtp_port` | `2525` | SMTP-Daemon-Port (Neustart nötig) | + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Admin-Bereich als eigene Next.js-Route** | Klare Trennung von User-Frontend – RoleGuard blockiert Nicht-Admins sofort | +| **Dashboard-Stats vom Backend** | Mail-Count, Speicher aus DB/Dateisystem – kein Client-seitiges Berechnen | +| **Settings als DB-Key-Value** | Einstellungen zur Laufzeit änderbar ohne Dateisystem-Zugriff oder Neustart (außer SMTP-Port) | +| **DSGVO-Anonymisierung statt Hard-Delete** | Archiv-Integrität bleibt erhalten – E-Mails im Archiv haben keinen Personenbezug mehr nach Anonymisierung | +| **Letzter-Admin-Schutz** | Verhindert Aussperrung – Backend prüft vor jedem Delete/Deaktivieren | + +### Abhängigkeiten + +**Next.js:** shadcn/ui Table, Dialog, Form (bereits installiert). +**Go Backend:** Nur pgx + Stdlib (bereits vorhanden). + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-11-audit-log.md b/features/PROJ-11-audit-log.md new file mode 100644 index 0000000..07907dd --- /dev/null +++ b/features/PROJ-11-audit-log.md @@ -0,0 +1,145 @@ +# PROJ-11: Audit-Log & Compliance-Berichte + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-13 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – Audit-Einträge sind an Nutzer geknüpft + +## User Stories +- Als Admin möchte ich sicherheitsrelevante Ereignisse im Audit-Log einsehen, damit ich Zugriffe und Änderungen nachvollziehen kann. +- Als Admin möchte ich den Audit-Log nach Datum, Nutzer und Ereignistyp filtern. +- Als Admin möchte ich den Audit-Log als CSV exportieren, damit ich ihn für externe Prüfungen verwenden kann. + +## Erfasste Ereignisse +**Ja – wird geloggt:** +- Login (Erfolg und Fehlschlag) inkl. IP-Adresse +- Logout +- Suchanfragen (Suchbegriff, Anzahl Treffer, Nutzer) +- Import gestartet / abgeschlossen / fehlgeschlagen (Quelle, Anzahl E-Mails) +- Export gestartet / abgeschlossen (Format, Anzahl E-Mails) + +**Nein – wird nicht geloggt:** +- Lesezugriff auf einzelne E-Mails (kein per-Mail-Leselogging) + +## Acceptance Criteria +- [ ] Jeder Log-Eintrag enthält: Zeitstempel (UTC), Nutzer-ID, Ereignistyp, Details (z.B. Suchbegriff, Import-Quelle), IP-Adresse +- [ ] Audit-Log-Ansicht **nur für Auditoren** unter `/audit/` – kein Zugriff für `admin` oder `user` +- [ ] Filterung nach: Datum (von–bis), Nutzer, Ereignistyp +- [ ] Paginierung (50 Einträge pro Seite) +- [ ] Export als CSV (gefilterte oder vollständige Ansicht, Streaming-Download) +- [ ] Einträge sind unveränderlich: kein UPDATE/DELETE durch Admin oder Anwendung möglich +- [ ] Retention konfigurierbar in `config.yml` (`audit.retention_days`), kein Standardwert erzwungen +- [ ] Doppelte Speicherung: PostgreSQL (für GUI-Abfragen) + Append-only Logdatei auf Disk (als unveränderliches Backup) +- [ ] Logdatei-Format: JSON Lines (ein Eintrag pro Zeile) + +## Edge Cases +- Audit-Log über Jahre sehr groß → paginierte DB-Abfragen mit Index auf `(timestamp, event_type, user_id)`, kein Full-Table-Scan +- Nutzer DSGVO-gelöscht → Audit-Einträge behalten, `user_id` durch `"anonymized"` ersetzen, IP-Adresse löschen +- Logdatei nicht beschreibbar beim Start → Warnung loggen, Dienst läuft weiter (DB-Log bleibt aktiv) +- Gleichzeitige Schreibzugriffe auf Logdatei → Append mit file lock + +## Technical Requirements +- PostgreSQL: separate Tabelle `audit_log`, kein DELETE/UPDATE per DB-Constraint (Row-Level Security oder Trigger) +- Logdatei: `/var/log/archivmail/audit.log` (Pfad konfigurierbar), append-only, JSON Lines +- Log-Rotation über `logrotate` (extern konfiguriert), Datei wird nie vom Dienst selbst rotiert oder gelöscht +- Zeitstempel immer UTC (RFC 3339) + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend (`/audit/*`):** +``` +/audit ← nur für Auditoren (RoleGuard) +└── AuditTable + ├── Spalten: Zeitstempel, Nutzer, Ereignis, Details, IP + ├── Filter-Leiste + │ ├── Datepicker: von – bis + │ ├── Dropdown: Nutzer auswählen + │ └── Dropdown: Ereignistyp + ├── Paginierung (50 pro Seite) + └── [CSV exportieren] Button → Streaming-Download +``` + +**Go Backend:** +``` +Audit-Service (intern, kein eigener HTTP-Handler) +├── WriteEvent(event AuditEvent) +│ ├── → INSERT INTO audit_log (kein UPDATE/DELETE je möglich) +│ └── → Append zu /var/log/archivmail/audit.log (JSON Line) +│ └── File-Lock beim Schreiben (sync.Mutex) +│ +└── Audit-API (nur für Auditoren) + ├── GET /api/audit/events ← Paginiert, gefiltert + └── GET /api/audit/export ← Streaming-CSV-Download +``` + +### Datenmodell + +**Tabelle `audit_log`** (append-only via DB-Trigger): + +| Feld | Beschreibung | +|------|-------------| +| `id` | Sequentielle ID | +| `timestamp` | UTC, RFC 3339 | +| `user_id` | Nutzer-ID (NULL nach DSGVO-Löschung) | +| `user_email` | E-Mail zum Zeitpunkt des Events (für Lesbarkeit nach Anonymisierung) | +| `event_type` | `login_ok`, `login_fail`, `logout`, `search`, `import_start`, `import_done`, `import_fail`, `export_start`, `export_done` | +| `details` | JSON: Suchbegriff / Import-Quelle / Anzahl / etc. | +| `ip_address` | IPv4/IPv6 (NULL nach DSGVO-Löschung) | + +**DB-Constraint:** PostgreSQL-Trigger verhindert `UPDATE` und `DELETE` auf der gesamten Tabelle → physische Unveränderlichkeit. + +**Logdatei-Format** (`/var/log/archivmail/audit.log`, JSON Lines): +``` +{"ts":"2024-03-01T10:00:00Z","user":"alice@firma.de","event":"search","details":{"q":"Rechnung","hits":42},"ip":"192.168.1.1"} +``` + +### Schreibfluss + +``` +Beliebige Aktion (Login, Suche, Import...) + │ + ▼ +audit.WriteEvent() aufgerufen + │ + ├── PostgreSQL INSERT (non-blocking, Goroutine) + │ + └── File-Append mit sync.Mutex + (Datei nicht beschreibbar? → Warnung auf stderr, Dienst läuft weiter) +``` + +### DSGVO-Löschfluss (Nutzer anonymisieren) + +``` +DELETE /api/admin/users/{id} + │ + ├── audit_log: user_id → NULL, ip_address → NULL + │ user_email → "anonymized" + └── Logdatei: bleibt unverändert (tamper-evident) +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Audit-Ansicht nur für Auditoren** | Strict role separation — Admin hat keine Einsicht in Zugriffsprotokolle | +| **DB-Trigger für Unveränderlichkeit** | Applikationscode kann versehentlich löschen — Trigger ist eine härtere Garantie | +| **Doppelte Speicherung** | DB für GUI-Abfragen; Logdatei als tamper-evident Backup für externe Prüfungen | +| **logrotate extern** | Dienst rotiert nie selbst — Logdatei bleibt unter Systemadmin-Kontrolle | +| **DSGVO: IP-Adresse löschen, Event behalten** | Personenbezug entfernen, Compliance-Nachweis bleibt erhalten | +| **Composite Index `(timestamp, event_type, user_id)`** | Schnelle gefilterte Abfragen auch bei sehr großem Log über Jahre | + +### Abhängigkeiten + +**Go Backend:** Nur Stdlib + pgx (bereits vorhanden). +**Next.js:** shadcn/ui Table, Select, DatePicker (bereits installiert). + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-12-export.md b/features/PROJ-12-export.md new file mode 100644 index 0000000..c9164f5 --- /dev/null +++ b/features/PROJ-12-export.md @@ -0,0 +1,122 @@ +# PROJ-12: E-Mail-Export (EML / PDF) + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-13 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) +- Requires: PROJ-6 (Volltext-Suche) – Export aus Suchergebnissen +- Requires: PROJ-7 (E-Mail-Ansicht) +- Requires: PROJ-11 (Audit-Log) – Export-Aktionen werden geloggt + +## User Stories +- Als Nutzer möchte ich eine einzelne E-Mail als EML-Datei exportieren, damit ich sie in einem E-Mail-Client öffnen kann. +- Als Nutzer möchte ich eine E-Mail als PDF drucken/exportieren, damit ich sie für Behörden oder Verträge verwenden kann. +- Als Admin möchte ich mehrere E-Mails als ZIP-Archiv exportieren, damit ich bei einer Anfrage mehrere Mails auf einmal liefern kann. +- Als System möchte ich jeden Export im Audit-Log erfassen. + +## Acceptance Criteria +- [ ] Einzelexport EML: Original-MIME-Inhalt wird unverändert heruntergeladen +- [ ] Einzelexport PDF: E-Mail-Header + Body als gut lesbares PDF gerendert, Anhänge als separate Dateien erwähnt +- [ ] Massenexport: Auswahl mehrerer E-Mails (Checkbox in Suchergebnissen), ZIP-Download +- [ ] ZIP enthält: EML-Dateien + optionale Anhänge + Manifest (CSV mit Metadaten) +- [ ] Massenexport-Limit konfigurierbar (Standard: max. 500 E-Mails pro Export) +- [ ] Jeder Export wird im Audit-Log erfasst (Nutzer, Anzahl E-Mails, Format) +- [ ] Zugriffsschutz: Nutzer kann nur eigene E-Mails exportieren + +## Edge Cases +- Export von 500 E-Mails mit großen Anhängen → Streaming-ZIP, kein Speicher-Overflow +- PDF-Rendering von komplexem HTML → graceful Fallback auf Plain-Text-PDF +- Nutzer wählt E-Mails aus, auf die er keinen Zugriff hat → diese werden aus Export-Liste entfernt + +## Technical Requirements +- ZIP-Erstellung als Stream (nicht komplett in Memory) +- PDF-Generierung serverseitig (z.B. wkhtmltopdf oder Go-PDF-Bibliothek) + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend:** +``` +Suchergebnisse (PROJ-6, Erweiterung) +├── Checkbox pro Treffer (Multi-Select) +├── [Exportieren] Button (aktiv wenn ≥1 ausgewählt) +└── Export-Dialog + ├── Format: EML | PDF + ├── Anhänge einschließen: ja / nein + └── [Download starten] → POST /api/export/zip (Streaming) + +E-Mail-Ansicht (PROJ-7, Erweiterung) +├── [Als EML herunterladen] Button → GET /api/export/eml/{id} +└── [Als PDF exportieren] Button → GET /api/export/pdf/{id} +``` + +**Go Backend:** +``` +Export-Handler +├── GET /api/export/eml/{id} ← Einzelexport EML +│ └── Original-.m-Datei lesen, AES-256-GCM entschlüsseln, direkt streamen +│ +├── GET /api/export/pdf/{id} ← Einzelexport PDF +│ ├── .m-Datei lesen + entschlüsseln +│ ├── Header + Body extrahieren +│ └── → PDF-Bibliothek → PDF streamen +│ Fallback: Plain-Text-PDF wenn HTML zu komplex +│ +└── POST /api/export/zip ← Massenexport + ├── Body: { ids: [...], format: "eml"|"pdf", attachments: bool } + ├── Zugriffscheck: Nutzer darf nur eigene Mails exportieren + ├── Max-Limit prüfen (konfigurierbar, Standard: 500) + ├── Streaming-ZIP (archive/zip Writer → ResponseWriter direkt) + │ ├── Pro Mail: .eml oder .pdf + │ ├── Anhänge: attachments// (wenn aktiviert) + │ └── manifest.csv (Message-ID, From, To, Subject, Date, Dateiname) + └── → Audit-Log: export_start + export_done (Anzahl, Format) +``` + +### Export-Fluss (Massenexport) + +``` +POST /api/export/zip + │ + ├── Zugriffsfilter: IDs auf user-eigene Mails beschränken + │ + ├── Audit-Log: export_start + │ + ├── ZIP-Stream öffnen (Content-Type: application/zip) + │ + └── Für jede Mail: + .m-Datei lesen → AES-256-GCM entschlüsseln + → Zu ZIP hinzufügen (kein vollständiges RAM-Buffering) + ↓ + manifest.csv Zeile anhängen + ↓ + ZIP schließen → Audit-Log: export_done +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Streaming-ZIP** | 500 Mails mit Anhängen können mehrere GB sein — kein RAM-Overhead | +| **Serverseitiges PDF** | Browser-Print ist nicht reproduzierbar; serverseitiges PDF ist auditierbar und einheitlich | +| **Plain-Text-Fallback für PDF** | Komplexes HTML kann PDF-Renderer zum Absturz bringen — graceful degradation | +| **Zugriffscheck im Export-Handler** | Serverseitige Filterung verhindert Datenlecks durch manipulierte IDs | +| **manifest.csv im ZIP** | Nachvollziehbarkeit bei Behördenanfragen ohne jede EML einzeln öffnen zu müssen | +| **Audit-Log für jeden Export** | Compliance-Anforderung — wer hat wann was exportiert | + +### Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `archive/zip` (Stdlib) | Streaming-ZIP-Erstellung ohne externe Abhängigkeit | +| `github.com/SebastiaanKlippert/go-wkhtmltopdf` | PDF-Generierung aus HTML (serverseitig) | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-13-rest-api-crm.md b/features/PROJ-13-rest-api-crm.md new file mode 100644 index 0000000..53e4c19 --- /dev/null +++ b/features/PROJ-13-rest-api-crm.md @@ -0,0 +1,178 @@ +# PROJ-13: REST API für externe CRM-Anbindung + +## Status: In Progress +**Created:** 2026-03-13 +**Last Updated:** 2026-03-13 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – API-Keys sind an Nutzer/Rollen gebunden +- Requires: PROJ-5 (Speicherung & Indexierung) – Daten kommen aus dem Archiv +- Requires: PROJ-6 (Volltext-Suche) – Suche über API nutzbar + +## Hinweis +Externe Systeme (CRM, ERP, Helpdesk etc.) sollen über eine dokumentierte REST API auf das Archiv zugreifen können – **ausschließlich lesend**. Schreiboperationen (Importieren, Löschen, Labeln etc.) sind über die API nicht möglich und werden nicht implementiert. Authentifizierung über API-Keys, nicht über Session-Cookies. + +## User Stories +- Als CRM-Administrator möchte ich einen API-Key generieren, damit mein CRM-System auf das Archiv zugreifen kann. +- Als CRM-System möchte ich E-Mails eines bestimmten Kontakts (E-Mail-Adresse) abrufen, damit ich die Kommunikationshistorie im CRM anzeigen kann. +- Als CRM-System möchte ich E-Mails nach Datum, Absender oder Betreff durchsuchen, damit ich gezielt relevante Mails finden kann. +- Als Admin möchte ich API-Keys verwalten (anlegen, deaktivieren, löschen), damit ich den Zugriff kontrollieren kann. +- Als Admin möchte ich sehen, welcher API-Key wann welche Anfragen gestellt hat. + +## Acceptance Criteria +- [ ] API-Key-Verwaltung im Admin-Bereich: anlegen, benennen, deaktivieren, löschen +- [ ] API-Keys haben eine konfigurierbare Rolle (`user` oder `auditor`) – bestimmt Zugriffsumfang (Leserechte) +- [ ] Nur `GET`-Methoden erlaubt – `POST`, `PUT`, `PATCH`, `DELETE` geben generisch 405 Method Not Allowed zurück +- [ ] Authentifizierung via HTTP-Header: `Authorization: Bearer ` +- [ ] Endpunkt: `GET /api/v1/mails?from=&to=&subject=&date_from=&date_to=` – Suche/Filterung +- [ ] Endpunkt: `GET /api/v1/mails/{message_id}` – einzelne E-Mail abrufen (Metadaten) +- [ ] Endpunkt: `GET /api/v1/mails/{message_id}/raw` – Original-EML herunterladen +- [ ] Endpunkt: `GET /api/v1/mails?contact=email@firma.de` – alle Mails eines Kontakts (From oder To) +- [ ] Antwortformat: JSON für Metadaten, `application/octet-stream` für Raw-EML +- [ ] Paginierung: `?page=1&limit=25` (max. 100 pro Anfrage) +- [ ] API-Zugriffe werden im Audit-Log erfasst (API-Key-Name, Endpunkt, Zeitstempel) +- [ ] OpenAPI/Swagger-Dokumentation unter `/api/v1/docs` + +## Edge Cases +- Ungültiger oder deaktivierter API-Key → 401 Unauthorized +- API-Key mit `user`-Rolle fragt Mails ab, auf die er keinen Zugriff hat → 403 +- Rate-Limiting: zu viele Anfragen pro API-Key → 429 Too Many Requests +- Sehr große Ergebnismengen (>10.000 Treffer) → Paginierung erzwingen, kein Full-Dump +- CRM fragt nicht existierende Message-ID ab → 404 + +## Technical Requirements +- **Reine Lese-API** – ausschließlich `GET`-Endpunkte, keine Schreiboperationen +- Eigener Route-Prefix `/api/v1/` für externe API (getrennt von interner `/api/`) +- API-Keys: zufällig generiert (32 Byte, Base64), bcrypt-gehasht in der DB (nie im Klartext) +- Rate-Limiting pro API-Key konfigurierbar (Standard: 60 Anfragen/Minute) +- OpenAPI 3.0 Spec wird aus Code generiert oder manuell gepflegt +- Versionierung: `/api/v1/` – spätere Versionen brechen bestehende Clients nicht + +--- +## Tech Design (Solution Architect) + +### Systemübersicht + +``` +CRM / ERP / Helpdesk + │ + │ GET /api/v1/mails?contact=kunde@example.com + │ Authorization: Bearer + ▼ +Go Backend – Externer API-Router (/api/v1/*) + │ + ├── API-Key Middleware ← statt Session-Cookie + ├── Rate Limiter + ├── Shared Search Service ←──── dieselbe Logik wie interne Suche (PROJ-6) + └── Shared Mail Service ←──── dieselbe Logik wie Mail-Abruf (PROJ-7) +``` + +### Komponentenstruktur + +**Go Backend:** +``` +/api/v1/* (externer Prefix, getrennt von internem /api/*) +│ +├── API-Key Middleware ← ersetzt Session-Middleware +│ ├── Bearer-Token aus Header lesen +│ ├── SHA-256-Hash → DB-Lookup ← schneller Lookup ohne bcrypt-Overhead +│ ├── Key deaktiviert? → 401 +│ ├── Rolle laden (user/auditor) +│ └── Rate-Limit-Konfiguration laden +│ +├── Rate Limiter ← Token-Bucket pro API-Key +│ └── Limit überschritten → 429 + Retry-After Header +│ +├── Method Guard ← alles außer GET → 405 +│ +├── Shared Search Service ← identische Logik wie /api/search (PROJ-6) +│ ├── Xapian QueryParser +│ ├── Rollen-Filter (user/auditor) +│ └── PostgreSQL Metadaten-Lookup +│ +├── Shared Mail Service ← identische Logik wie /api/mails (PROJ-7) +│ ├── .m-Datei lesen + entschlüsseln +│ ├── MIME-Parser +│ └── Anhang-Streaming +│ +├── Audit Logger ← API-Key-Name + Endpunkt + Zeitstempel +│ +└── API-Key-Verwaltung (Admin) + ├── POST /api/admin/apikeys ← Key generieren (einmalige Anzeige) + ├── GET /api/admin/apikeys ← Liste (Name + Rolle + letzter Zugriff) + └── DELETE /api/admin/apikeys/{id} +``` + +### API-Key Authentifizierungsfluss + +``` +CRM-System + │ + │ Authorization: Bearer am_a1b2c3d4e5f6... + ▼ +API-Key Middleware + ├─ Präfix "am_" prüfen + ├─ SHA-256(token) → DB-Lookup (indiziert) + ├─ Key gefunden + aktiv? Nein → 401 + └─ Ja → Rolle + Rate-Limit laden + │ + ▼ +Rate Limiter (Token-Bucket) + ├─ Limit erreicht? → 429 + Retry-After: 30 + └─ OK → weiter + │ + ▼ +Method Guard + ├─ POST/PUT/DELETE? → 405 + └─ GET → weiter + │ + ▼ +Shared Service Layer + │ + ▼ +Audit Logger → API-Key-Name + Endpunkt + Zeitstempel +``` + +### Datenmodell + +**Tabelle `api_keys`:** + +| Feld | Beschreibung | +|------|-------------| +| `id` | Interne ID | +| `name` | Bezeichnung (z.B. "CRM Salesforce") | +| `token_hash` | SHA-256 des Tokens (für schnellen Lookup, indiziert) | +| `role` | `user` oder `auditor` | +| `active` | `true` / `false` | +| `rate_limit` | Anfragen pro Minute (Standard: 60) | +| `created_at` | Erstellungszeitpunkt | +| `last_used_at` | Letzter erfolgreicher Zugriff | + +**Key-Format:** +``` +Generiert: am_<32-Byte-random-Base64> +Gespeichert: SHA-256(token) in DB +Angezeigt: einmalig im Admin-UI – danach nicht mehr abrufbar +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Shared Service Layer** | Suche und Mail-Abruf teilen dieselbe Go-Logik mit der internen API – kein doppelter Code | +| **SHA-256 statt bcrypt** | API-Keys sind kryptografisch zufällig (32 Byte) – SHA-256 reicht, bcrypt wäre bei jeder Anfrage zu langsam | +| **`am_`-Präfix** | Erkennungsmerkmal für archivmail-Keys – einfach filterbar in Logs | +| **Token einmalig anzeigen** | Nur Hash gespeichert – kein späteres Auslesen möglich (wie GitHub PAT) | +| **Token-Bucket Rate Limiter** | Gleichmäßige Anfragen erlaubt, kurze Bursts toleriert | +| **`/api/v1/` Prefix** | Klare Versionierung – zukünftige `/api/v2/` bricht bestehende Clients nicht | +| **Audit-Log bei API-Zugriffen** | Externe Zugriffe werden geloggt (anders als interne Lesezugriffe) | + +### Abhängigkeiten + +Kein zusätzliches Paket – Rate-Limiter und SHA-256 aus der Go-Stdlib (`crypto/sha256`, `sync`). + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-14-import-pop3.md b/features/PROJ-14-import-pop3.md new file mode 100644 index 0000000..6aa8cfb --- /dev/null +++ b/features/PROJ-14-import-pop3.md @@ -0,0 +1,147 @@ +# PROJ-14: E-Mail-Import: POP3-Verbindung + +## Status: In Progress +**Created:** 2026-03-13 +**Last Updated:** 2026-03-13 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – nur Admins verwalten POP3-Verbindungen +- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert + +## Hinweis +POP3 kennt keine Ordnerstruktur – es gibt nur eine Inbox. Alle Mails werden importiert. Da POP3 keine UID-basierte Synchronisation unterstützt, ist nur ein einmaliger Initial-Import sinnvoll (kein regelmäßiger Sync wie bei IMAP). + +## User Stories +- Als Admin möchte ich einen POP3-Server konfigurieren (Host, Port, Zugangsdaten), damit ich E-Mails von dort importieren kann. +- Als System möchte ich alle vorhandenen E-Mails vom POP3-Server herunterladen und archivieren. +- Als Admin möchte ich den Verbindungsstatus und Importfortschritt sehen. +- Als System möchte ich Duplikate (gleiche Message-ID) überspringen. + +## Acceptance Criteria +- [ ] Konfigurationsformular: Host, Port, Verbindungsmodus (SSL/TLS, STARTTLS, None), Benutzername, Passwort +- [ ] **Verbindungsmodi:** + - `SSL/TLS` – direkte TLS-Verbindung (Port 995) + - `STARTTLS` – startet unverschlüsselt, wird auf TLS hochgestuft (Port 110) + - `None` – unverschlüsselt, nur für lokale/Testumgebungen +- [ ] Verbindungstest vor dem Speichern (Timeout: 10 Sekunden) +- [ ] Passwörter AES-256-GCM verschlüsselt in der DB gespeichert +- [ ] Import: alle Mails vom Server herunterladen +- [ ] Duplikate (Message-ID) werden übersprungen +- [ ] Fortschrittsanzeige während Import (X von Y Mails) +- [ ] Abschlussbericht: importiert / übersprungen / Fehler +- [ ] Mails bleiben nach dem Import auf dem POP3-Server (kein DELE-Befehl) + +## Edge Cases +- POP3-Server nicht erreichbar → Fehlermeldung mit Retry-Option +- Falsche Zugangsdaten → klare Fehlermeldung +- Mail ohne Message-ID → synthetische ID generieren (SHA-256 des Inhalts) +- Verbindungsabbruch während Import → bei Neustart von vorne (POP3 hat keine UIDs zum Weiterführen) +- Sehr großes Postfach (10.000+ Mails) → sequenzielles Herunterladen, kein Speicher-Overflow + +## Technical Requirements +- **Verbindungsmodi:** SSL/TLS (Port 995), STARTTLS (Port 110), None +- POP3 unterstützt keine Ordner – es gibt nur die Inbox, keine Ordner-Erkennung nötig +- Kein regelmäßiger Sync – nur manueller Import (POP3 bietet keine zuverlässige Duplikatserkennung über Sessions hinaus) +- Zugangsdaten AES-256-GCM verschlüsselt in der DB + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend (Admin-Bereich):** +``` +/admin/pop3 +├── POP3-Verbindungsliste +│ └── VerbindungsCard +│ ├── Name, Host, Status +│ ├── Letzter Import + Anzahl +│ └── Aktionen: Bearbeiten / Löschen / Import starten +├── Verbindung-Formular +│ ├── Host, Port, Verbindungsmodus (SSL/TLS | STARTTLS | None) +│ ├── Benutzername, Passwort +│ └── [Verbindung testen] Button +└── Import-Fortschrittsanzeige + ├── Fortschrittsbalken + └── Abschlussbericht +``` + +**Go Backend:** +``` +POP3-Dienst +├── POST /api/admin/pop3 ← Verbindung anlegen +├── POST /api/admin/pop3/test ← Verbindung testen +├── GET /api/admin/pop3 ← auflisten +├── DELETE /api/admin/pop3/{id} ← löschen +│ +├── POP3-Client +│ ├── SSL/TLS + STARTTLS Handler +│ ├── USER/PASS Login +│ ├── STAT → Anzahl Mails + Gesamtgröße +│ ├── LIST → Message-Nummern +│ └── RETR → Mail herunterladen (kein DELE) +│ +└── Import-Worker (Hintergrund-Goroutine) + ├── Sequenziell: RETR 1, RETR 2, ... + ├── Duplikat-Check (Message-ID) + ├── → Storage Coordinator (PROJ-5) + └── Fortschritt in DB +``` + +### Verbindungsmodus-Übersicht + +| Modus | Port | Ablauf | +|-------|------|--------| +| `SSL/TLS` | 995 | TLS direkt beim Verbindungsaufbau | +| `STARTTLS` | 110 | Verbindung startet plain → STLS-Befehl → TLS | +| `None` | 110 | Unverschlüsselt (nur Testumgebung) | + +### Importfluss + +``` +Admin klickt "Import starten" + │ + ▼ +POP3-Client verbindet (SSL/TLS oder STARTTLS) + │ + ▼ +STAT → Gesamtanzahl Mails (z.B. 3.842) + │ + ▼ +LIST → Message-Nummern [1, 2, 3, ..., 3842] + │ + ▼ +Für jede Message-Nummer: + RETR → rohe Mail (RFC 2822) + Message-ID Duplikat? → überspringen + → Storage Coordinator (PROJ-5) + Fortschritt: n / 3842 + │ + ▼ +Kein DELE → Mails bleiben auf dem Server + │ + ▼ +QUIT → Verbindung trennen +Abschlussbericht speichern +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Kein DELE** | Archiv löscht nichts vom Quellserver – nur lesen und archivieren | +| **Kein regelmäßiger Sync** | POP3 hat keine UIDs – es gibt keine zuverlässige Möglichkeit festzustellen welche Mails bereits importiert wurden | +| **Synthetische Message-ID bei Fehlen** | POP3-Mails ohne Message-ID bekommen SHA-256(Inhalt) als ID – Duplikatserkennung bleibt konsistent | +| **Gleiche Codebasis wie IMAP-Worker** | Import-Worker-Struktur identisch – nur POP3-Client statt IMAP-Client | + +### Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `github.com/emersion/go-message` | POP3-Client mit TLS/STARTTLS | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-15-cli-import-export.md b/features/PROJ-15-cli-import-export.md new file mode 100644 index 0000000..3b842e4 --- /dev/null +++ b/features/PROJ-15-cli-import-export.md @@ -0,0 +1,195 @@ +# PROJ-15: CLI Import & Export + +## Status: In Progress +**Created:** 2026-03-13 +**Last Updated:** 2026-03-13 + +## Dependencies +- Requires: PROJ-5 (Speicherung & Indexierung) – Import nutzt Storage Coordinator +- Requires: PROJ-1 (Authentifizierung) – CLI läuft als Systembenutzer `archivmail`, kein Web-Login + +## Hinweis +Die CLI läuft direkt auf dem Server als Systembenutzer `archivmail` – kein Web-Login, kein API-Key. Zugriff über den gleichen Storage Coordinator wie der Daemon. Gedacht für automatisierte Skripte, Cron-Jobs und administrative Bulk-Operationen. + +## User Stories +- Als Systemadministrator möchte ich EML/MBOX-Dateien per CLI importieren, damit ich Bulk-Importe skriptbasiert automatisieren kann. +- Als Systemadministrator möchte ich E-Mails per CLI exportieren (EML/MBOX), damit ich Sicherungen oder Migrationen durchführen kann. +- Als Systemadministrator möchte ich Import/Export mit Pfadangabe starten, damit ich Quell- und Zielverzeichnisse flexibel festlegen kann. +- Als System möchte ich Import-Fortschritt und Ergebnis auf stdout ausgeben, damit Skripte den Status auswerten können. + +## Acceptance Criteria + +### Import +- [ ] `archivmail import --file /pfad/zu/datei.eml` – einzelne EML importieren +- [ ] `archivmail import --file /pfad/zu/archiv.mbox` – MBOX importieren +- [ ] `archivmail import --dir /pfad/zum/verzeichnis/` – alle EML-Dateien in einem Verzeichnis importieren (rekursiv optional: `--recursive`) +- [ ] Fortschrittsausgabe auf stdout (eine Zeile pro Mail oder Fortschrittsbalken) +- [ ] Exit-Code 0 bei Erfolg, 1 bei Fehler +- [ ] Duplikate werden übersprungen (gleiche Message-ID), kein Fehler +- [ ] `--dry-run` Flag: zeigt was importiert würde ohne tatsächlich zu speichern + +### Export +- [ ] `archivmail export --out /pfad/ziel/` – alle Mails als EML-Dateien exportieren +- [ ] `archivmail export --out /pfad/archiv.mbox` – alle Mails als MBOX exportieren +- [ ] `archivmail export --from alice@firma.de --out /pfad/` – Filter nach Absender +- [ ] `archivmail export --date-from 2024-01-01 --date-to 2024-12-31 --out /pfad/` – Filter nach Datum +- [ ] `archivmail export --query "Rechnung" --out /pfad/` – Filter per Volltext-Suche (Xapian) +- [ ] Exportierte Mails werden entschlüsselt (Klartext EML auf Disk) +- [ ] `--format eml` (Standard) oder `--format mbox` + +### Allgemein +- [ ] CLI läuft als Systembenutzer `archivmail` – liest Key aus `/etc/archivmail/keyfile` +- [ ] Fehler werden auf stderr ausgegeben +- [ ] `archivmail help` zeigt Übersicht aller Befehle +- [ ] `archivmail version` zeigt Version + +## Edge Cases +- Verzeichnis beim Import enthält keine EML-Dateien → Hinweis + Exit-Code 0 +- Zieldatei beim Export bereits vorhanden → Fehler mit `--force` Flag zum Überschreiben +- Kein Lese-/Schreibrecht auf Pfad → klare Fehlermeldung auf stderr +- Import unterbrochen (Ctrl+C) → partiell importierte Mails werden gespeichert, kein Rollback (Archiv ist append-only) +- Export bei leerem Archiv → leeres Verzeichnis / leere MBOX, Exit-Code 0 + +## Technical Requirements +- CLI ist Teil desselben Go-Binaries (`archivmail`) – Subcommands via `archivmail ` +- Zugriff auf Storage Coordinator direkt (kein HTTP-Umweg über den laufenden Daemon) +- Key-Datei muss lesbar sein (`/etc/archivmail/keyfile`, `chmod 400`, Owner `archivmail`) +- Kann parallel zum laufenden Daemon betrieben werden (Xapian WritableDatabase: Lock beachten) +- Strukturierte Ausgabe optional: `--json` Flag für maschinenlesbare Ausgabe + +--- +## Tech Design (Solution Architect) + +### CLI-Struktur + +``` +archivmail [flags] + +Commands: + import E-Mails importieren (EML, MBOX, Verzeichnis) + export E-Mails exportieren (EML, MBOX) + version Version anzeigen + help Hilfe anzeigen + +archivmail import + --file /pfad/datei.eml oder .mbox + --dir /pfad/verzeichnis/ + --recursive Unterverzeichnisse einschließen (mit --dir) + --dry-run Simulation ohne Speichern + --json Maschinenlesbare Ausgabe (JSON) + +archivmail export + --out /pfad/ziel/ oder /pfad/archiv.mbox (Pflicht) + --format eml (Standard) | mbox + --from Absender-Filter + --to Empfänger-Filter + --date-from Datum von (ISO 8601: 2024-01-01) + --date-to Datum bis (ISO 8601: 2024-12-31) + --query Volltext-Suche (Xapian QueryParser) + --force Zieldatei überschreiben + --json Maschinenlesbare Ausgabe (JSON) +``` + +### Komponentenstruktur + +``` +archivmail (Go-Binary) +│ +├── main.go ← Subcommand-Router (import / export / ...) +│ +├── cmd/import.go +│ ├── Flag-Parsing +│ ├── Dateityp-Erkennung (.eml / .mbox / Verzeichnis) +│ ├── EML-Parser +│ ├── MBOX-Parser (zeilenweise) +│ └── → Storage Coordinator (PROJ-5, direkt, kein HTTP) +│ +└── cmd/export.go + ├── Flag-Parsing + ├── Filter-Builder (from, to, date, query) + ├── → Xapian ReadonlyDatabase (Suche/Filter) + ├── → PostgreSQL Metadaten-Lookup + ├── → .m-Datei lesen + AES-256-GCM entschlüsseln + └── Schreiben als EML-Dateien oder MBOX +``` + +### Import-Fluss + +``` +$ archivmail import --dir /backup/mails/ --recursive + +Key laden aus /etc/archivmail/keyfile +Verzeichnis scannen → 3.842 .eml-Dateien gefunden +[████████░░] 2.150 / 3.842 (übersprungen: 12 Duplikate) + +Fertig: + Importiert: 2.130 + Übersprungen: 12 (Duplikate) + Fehler: 0 +``` + +### Export-Fluss + +``` +$ archivmail export --from alice@firma.de \ + --date-from 2024-01-01 \ + --out /backup/export/ + +Key laden aus /etc/archivmail/keyfile +Xapian: 847 Mails gefunden (Filter: from=alice, date>=2024-01-01) +Exportiere nach /backup/export/ +[████████████] 847 / 847 + +Fertig: + Exportiert: 847 EML-Dateien + Ziel: /backup/export/ +``` + +### JSON-Ausgabe (--json Flag) + +```json +{ + "status": "done", + "imported": 2130, + "skipped": 12, + "errors": 0, + "duration_sec": 42 +} +``` + +### Xapian-Lock beim parallelen Betrieb + +``` +Daemon läuft (WritableDatabase hält Lock für Index-Worker) + │ +CLI export → ReadonlyDatabase → kein Lock-Konflikt ✓ +CLI import → Storage Coordinator → WritableDatabase + │ + └── Lock bereits gehalten? + → Warten (max. 30 Sek.) → dann Fehlermeldung: + "Index locked by running daemon. Stop daemon or retry." +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Gleiche Binary, Subcommands** | Kein separates CLI-Tool – `archivmail import` und `archivmail serve` teilen Code und Storage Coordinator | +| **Direkter Speicherzugriff, kein HTTP** | CLI läuft als `archivmail`-User mit Dateisystem-Zugriff – kein laufender Daemon nötig für Import/Export | +| **`--dry-run`** | Sicher testen ohne Daten zu verändern – wichtig für große Bulk-Imports | +| **`--json` Flag** | Maschinenlesbar für Cron-Jobs, Monitoring-Skripte, Ansible-Playbooks | +| **Exit-Codes** | 0 = Erfolg, 1 = Fehler – Standard für Shell-Skripting | +| **Xapian ReadonlyDatabase für Export** | Export kann parallel zum Daemon laufen ohne Lock-Konflikte | + +### Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `github.com/spf13/cobra` | Subcommand-CLI-Framework | +| Xapian CGo-Bindings | Volltext-Filter beim Export (bereits PROJ-5) | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-16-ldap-active-directory.md b/features/PROJ-16-ldap-active-directory.md new file mode 100644 index 0000000..da2a69a --- /dev/null +++ b/features/PROJ-16-ldap-active-directory.md @@ -0,0 +1,109 @@ +--- +id: PROJ-16 +title: LDAP / Active Directory Anbindung +status: In Progress +priority: P1 +created: 2026-03-13 +--- + +# PROJ-16 — LDAP / Active Directory Anbindung + +## Ziel +Authentifizierung gegen einen LDAP-Server (OpenLDAP, Microsoft Active Directory, Samba AD). +Lokale Accounts bleiben weiterhin nutzbar. LDAP-User werden beim ersten Login automatisch +in der Datenbank angelegt (`source: ldap`) und bei jedem Login synchronisiert. + +## User Stories + +- **Als Admin** möchte ich LDAP in `config.yml` konfigurieren, damit Mitarbeiter ihre + bestehenden Windows/AD-Zugangsdaten nutzen können. +- **Als Endnutzer** möchte ich mich mit meinem AD-Passwort anmelden, ohne einen separaten + archivmail-Account zu benötigen. +- **Als Admin** möchte ich LDAP-User einer Rolle (user/auditor/admin) zuweisen können, + entweder per fester Zuordnung oder über AD-Gruppen. +- **Als Admin** möchte ich LDAP deaktivieren können, ohne den restlichen Betrieb zu stören. + +## Akzeptanzkriterien + +- [ ] Login mit LDAP-Credentials funktioniert wenn `ldap.enabled: true` +- [ ] Lokale Accounts funktionieren weiterhin (Fallback wenn LDAP fehlschlägt oder deaktiviert) +- [ ] LDAP-User werden beim Login automatisch via `UpsertLDAPUser` angelegt/aktualisiert +- [ ] Rollen-Mapping via AD-Gruppen konfigurierbar (optional, Fallback: default_role) +- [ ] STARTTLS und LDAPS (Port 636) werden unterstützt +- [ ] Bind-User (Service Account) für AD-Suche konfigurierbar +- [ ] Fehlermeldung bei falschem Passwort ist identisch zu lokalem Login (kein Info-Leak) +- [ ] LDAP-Fehler landen im Audit-Log +- [ ] Konfigurierbar per `config.yml` Abschnitt `ldap:` + +## Konfigurationsformat (`config.yml`) + +```yaml +ldap: + enabled: true + url: "ldap://192.168.1.10:389" # oder ldaps://... + bind_dn: "CN=archivmail-svc,OU=ServiceAccounts,DC=corp,DC=local" + bind_password: "geheim" + base_dn: "OU=Users,DC=corp,DC=local" + user_filter: "(sAMAccountName=%s)" # %s wird durch eingegebenen Username ersetzt + tls: false # STARTTLS + tls_skip_verify: false + default_role: "user" # Rolle für neue LDAP-User + group_mappings: # optional: AD-Gruppe → archivmail-Rolle + - group_dn: "CN=archivmail-admins,OU=Groups,DC=corp,DC=local" + role: "admin" + - group_dn: "CN=archivmail-auditors,OU=Groups,DC=corp,DC=local" + role: "auditor" +``` + +## Technische Umsetzung + +### Neues Paket: `internal/ldapauth` + +``` +internal/ldapauth/ + ldap.go — Client, Bind, Search, Authenticate + ldap_test.go — Tests mit Mock-LDAP +``` + +**Abhängigkeit:** `github.com/go-ldap/ldap/v3` + +### Ablauf Login mit LDAP + +1. `auth.Manager.Login(username, password)` prüft zuerst lokale DB +2. Wenn lokaler User nicht gefunden UND LDAP aktiviert → LDAP-Auth versuchen +3. LDAP-Bind mit Service Account → User-DN per `user_filter` suchen +4. User-Bind mit gefundener DN + eingegebenem Passwort +5. Optional: Gruppen-Mitgliedschaft abfragen → Rolle bestimmen +6. `userstore.UpsertLDAPUser(username, email, role)` aufrufen +7. JWT-Token wie bei lokalem Login ausstellen + +### Felder aus LDAP lesen + +| LDAP-Attribut | archivmail-Feld | +|--------------|-----------------| +| `sAMAccountName` / `uid` | username | +| `mail` | email | +| `memberOf` | → Gruppen-Mapping → role | +| `displayName` | (für spätere Anzeige) | + +### API-Endpunkt: `GET /api/admin/ldap/test` (admin only) + +Testet die LDAP-Verbindung und gibt Status zurück: +```json +{"ok": true, "message": "LDAP-Verbindung erfolgreich", "users_found": 42} +``` + +## Nicht in diesem Feature + +- Automatische User-Synchronisation (Bulk-Import aller AD-User) — separates Feature +- LDAP-Gruppen als Postfach-Zuweisungen +- Kerberos / SAML / OAuth2 (separate Features) + +## Dateien + +- `internal/ldapauth/ldap.go` (neu) +- `internal/auth/auth.go` (erweitert: LDAP-Fallback) +- `config/config.go` (erweitert: `LDAPConfig`) +- `cmd/archivmail/main.go` (erweitert: LDAP-Client initialisieren) +- `internal/api/server.go` (erweitert: `/api/admin/ldap/test`) +- `install.sh` (erweitert: LDAP-Kommentar in config.yml) diff --git a/features/PROJ-17-system-dashboard.md b/features/PROJ-17-system-dashboard.md new file mode 100644 index 0000000..07df56f --- /dev/null +++ b/features/PROJ-17-system-dashboard.md @@ -0,0 +1,86 @@ +# PROJ-17: Admin Dashboard – Systemauslastung & Archiv-Übersicht + +## Status: In Review +**Created:** 2026-03-14 +**Last Updated:** 2026-03-14 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – nur Admins sehen das Dashboard +- Requires: PROJ-5 (Speicherung) – erste/letzte Mail aus dem Archiv + +## User Stories +- Als Admin möchte ich die aktuelle CPU-Auslastung sehen, damit ich Engpässe erkennen kann. +- Als Admin möchte ich die RAM-Auslastung (gesamt / verwendet / frei) sehen. +- Als Admin möchte ich alle eingebundenen Festplatten/Partitionen mit Füllstand sehen (Balken). +- Als Admin möchte ich die älteste und neueste archivierte Mail sehen (Datum, Von, Betreff), damit ich den Archivierungszeitraum auf einen Blick erkenne. + +## Acceptance Criteria +- [x] CPU: Load Average (1min / 5min / 15min) aus `/proc/loadavg` +- [x] RAM: MemTotal, MemUsed, MemAvailable aus `/proc/meminfo`; Prozentbalken +- [x] Disk: alle physischen Partitionen (keine tmpfs/proc/sysfs/devtmpfs/overlay) via `syscall.Statfs`; je Partition: Mountpoint, Gesamt, Belegt, Frei, Prozent +- [x] Erste Mail im Archiv: Datum, Von, Betreff (älteste Datei im Store) +- [x] Letzte Mail im Archiv: Datum, Von, Betreff (neueste Datei im Store) +- [x] Endpoint: `GET /api/admin/system/stats` (Admin-only) +- [x] Storage-Erweiterung: `store.FirstAndLastMail()` liefert Metadaten der ältesten und neuesten Mail + +## API Response Schema + +```json +{ + "cpu": { + "load1": 0.42, + "load5": 0.38, + "load15": 0.31, + "num_cpu": 4 + }, + "ram": { + "total_bytes": 8388608000, + "used_bytes": 3221225472, + "free_bytes": 5167382528, + "used_pct": 38.4 + }, + "disks": [ + { + "mount": "/", + "total_bytes": 53687091200, + "used_bytes": 12884901888, + "free_bytes": 40802189312, + "used_pct": 24.0, + "fstype": "ext4" + } + ], + "archive": { + "first_mail": { "id": "abc123", "date": "2024-01-15T08:00:00Z", "from": "...", "subject": "..." }, + "last_mail": { "id": "def456", "date": "2026-03-14T10:08:00Z", "from": "...", "subject": "..." } + } +} +``` + +## Technical Design + +### Backend (`internal/api/server.go`) +- Neuer Handler `handleSystemStats` +- CPU: `/proc/loadavg` parsen → load1, load5, load15 + `runtime.NumCPU()` +- RAM: `/proc/meminfo` parsen → MemTotal, MemFree, MemAvailable, Buffers, Cached + - `used = total - available` +- Disks: `/proc/mounts` lesen, für jeden Eintrag `syscall.Statfs()` aufrufen + - Ausschließen: fstype in {tmpfs, proc, sysfs, devtmpfs, cgroup, cgroup2, overlay, squashfs, debugfs, tracefs, securityfs, pstore, efivarfs, bpf, hugetlbfs, mqueue, ramfs} +- Erste/letzte Mail: `store.FirstAndLastMail()` → walk store dir, min/max ModTime + +### Storage (`internal/storage/storage.go`) +- Neue Methode `FirstAndLastMail() (*MailRef, *MailRef, error)` +- `MailRef{ID, ModTime}` → ID wird an `handleSystemStats` übergeben, der dann via `mailparser.Parse()` From+Subject+Date extrahiert + +### Frontend (`src/app/admin/page.tsx`) +- Neue Kacheln im Dashboard-Tab: + - **CPU-Auslastung**: Load Average mit `num_cpu` Kontext + - **Arbeitsspeicher**: Fortschrittsbalken (used/total), Zahlen darunter + - **Festplatten**: eine Karte pro Partition mit Balken + Zahlen + - **Archivzeitraum**: erste und letzte Mail als kompakte Zeilen (Datum · Von · Betreff) + +## Implementation Notes +- **Backend:** `handleSystemStats` in `internal/api/server.go` — CPU via `/proc/loadavg`, RAM via `/proc/meminfo`, alle Disks via `/proc/mounts` + `syscall.Statfs`, Archiv-Zeitspanne via `store.FirstAndLastMail()` +- **Storage:** `FirstAndLastMail()` + `MailRef` in `internal/storage/storage.go` — walkt Store-Verzeichnis, liefert älteste/neueste Mail per ModTime +- **Route:** `GET /api/admin/system/stats` (Admin-only, Token-Auth) +- **Frontend:** Dashboard-Tab in `src/app/admin/page.tsx` mit CPU, RAM, Disk-Partitionen und Archivzeitraum; Auto-Refresh alle 30 Sekunden +- **Bereit für Test auf** `root@192.168.1.131` diff --git a/features/PROJ-2-import-eml-mbox.md b/features/PROJ-2-import-eml-mbox.md new file mode 100644 index 0000000..112df65 --- /dev/null +++ b/features/PROJ-2-import-eml-mbox.md @@ -0,0 +1,146 @@ +# PROJ-2: E-Mail-Import: EML/MBOX Upload + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – nur eingeloggte Admins dürfen importieren +- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert + +## User Stories +- Als Admin möchte ich EML-Dateien per Drag-and-Drop hochladen, damit ich einzelne E-Mails archivieren kann. +- Als Admin möchte ich MBOX-Dateien importieren, damit ich ganze Postfach-Exporte auf einmal archivieren kann. +- Als Admin möchte ich den Fortschritt eines laufenden Imports sehen, damit ich weiß wie weit der Import ist. +- Als Admin möchte ich nach dem Import eine Zusammenfassung sehen (importiert, übersprungen, Fehler), damit ich Probleme nachvollziehen kann. +- Als System möchte ich Duplikate erkennen und überspringen, damit E-Mails nicht doppelt archiviert werden. + +## Acceptance Criteria +- [ ] Upload-Interface akzeptiert .eml und .mbox Dateien (auch mehrere gleichzeitig) +- [ ] Maximale Dateigröße konfigurierbar (Standard: 500 MB pro Upload) +- [ ] EML-Parser liest Envelope-Header (From, To, CC, BCC, Date, Subject, Message-ID) +- [ ] MBOX-Parser iteriert über alle enthaltenen E-Mails in der Datei +- [ ] Anhänge werden extrahiert und getrennt gespeichert +- [ ] Fortschrittsanzeige während des Imports (Anzahl verarbeitet / gesamt) +- [ ] Duplikate (gleiche Message-ID) werden erkannt und übersprungen +- [ ] Import-Zusammenfassung: Anzahl importiert, übersprungen, fehlerhaft +- [ ] Fehlerhafte E-Mails (korrupte Dateien) werden geloggt und übersprungen, brechen Import nicht ab + +## Edge Cases +- MBOX-Datei mit 100.000+ E-Mails → chunked processing, kein Timeout +- EML-Datei ohne Message-ID → synthetische ID generieren (Hash des Inhalts) +- E-Mail mit verschachtelten MIME-Teilen (multipart/mixed, multipart/alternative) +- Encoding-Probleme (ISO-8859-1, Windows-1252) → automatische Konvertierung zu UTF-8 +- Upload wird unterbrochen → partiell importierte Daten bereinigen + +## Technical Requirements +- Streaming-Upload für große Dateien (kein komplettes In-Memory-Laden) +- MBOX-Parsing als Background-Job mit Statusrückmeldung via WebSocket oder Polling +- Maximale Anhang-Größe pro E-Mail konfigurierbar + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend (Admin-Bereich):** +``` +/admin/upload +├── DropZone ← Drag-and-Drop + Datei-Dialog +│ ├── akzeptiert: .eml, .mbox +│ └── Mehrfachauswahl möglich +├── Upload-Queue ← Liste der hochgeladenen Dateien +│ └── FileItem (pro Datei) +│ ├── Dateiname + Größe +│ ├── Typ-Badge (EML / MBOX) +│ └── Status (wartend / läuft / fertig / Fehler) +├── Fortschrittsanzeige (pro Datei) +│ ├── Fortschrittsbalken (X von Y Mails verarbeitet) +│ └── Aktueller Status +└── Abschlussbericht + ├── Importiert: X + ├── Übersprungen (Duplikate): Y + └── Fehler: Z (mit Liste der fehlerhaften Mails) +``` + +**Go Backend:** +``` +POST /api/admin/upload +├── Session Middleware (Admin) +├── Multipart-Stream-Handler ← Datei wird nicht komplett in RAM geladen +├── Dateityp-Erkennung (.eml / .mbox) +└── Import-Worker starten (Hintergrund-Goroutine) + +Import-Worker +├── EML-Modus +│ └── Einzelne Mail direkt parsen → Storage Coordinator +│ +├── MBOX-Modus +│ ├── MBOX-Parser (iteriert über "From "-Trennzeilen) +│ ├── Für jede Mail: +│ │ ├── Duplikat-Check (Message-ID) +│ │ └── → Storage Coordinator (PROJ-5) +│ └── Fortschritt in DB schreiben +│ +└── Encoding-Normalisierer + └── ISO-8859-1 / Windows-1252 → UTF-8 + +GET /api/admin/upload/{job_id}/progress ← Polling alle 2 Sek. +``` + +### Upload- und Importfluss + +``` +Admin zieht Datei in DropZone + │ + │ POST /api/admin/upload (multipart/form-data, streaming) + ▼ +Go Backend empfängt Stream + │ + ├── .eml? → direkt parsen → Storage Coordinator → fertig + │ + └── .mbox? → Import-Worker (Hintergrund) + │ + ▼ + MBOX-Parser liest zeilenweise + Trenner: Zeilen die mit "From " beginnen + │ + └── Pro Mail: + Encoding-Erkennung + UTF-8-Normalisierung + Message-ID vorhanden? + Nein → SHA-256(Inhalt) als ID + Duplikat? → überspringen + → Storage Coordinator (PROJ-5) + Fortschritt in DB aktualisieren + │ + ▼ + Abschlussbericht in DB speichern + +Next.js pollt GET /progress alle 2 Sek. +→ Fortschrittsbalken aktualisieren +→ Bei status:"done" → Abschlussbericht anzeigen +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Streaming-Upload** | 500 MB MBOX nie komplett in RAM – Go liest den HTTP-Body als Stream direkt in den Parser | +| **MBOX zeilenweises Parsen** | MBOX-Format trennt Mails durch `From `–Zeilen – kein vollständiges Einlesen der Datei nötig | +| **Background-Worker + Polling** | MBOX mit 100k+ Mails dauert Minuten – HTTP-Request darf nicht so lange offen bleiben | +| **Encoding-Normalisierung** | E-Mail-Exporte aus Outlook/Thunderbird kommen oft als ISO-8859-1 – Index und DB erwarten UTF-8 | +| **Fehler überspringen, nicht abbrechen** | Eine korrupte Mail soll nicht den gesamten Import einer 50k-MBOX-Datei stoppen | +| **Synthetische Message-ID** | EML-Dateien ohne Message-ID (selten aber möglich) bekommen SHA-256(Inhalt) – Duplikatschutz bleibt konsistent | + +### Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `golang.org/x/text/encoding` | ISO-8859-1 / Windows-1252 → UTF-8 Konvertierung | +| `mime`, `mime/multipart` | EML + MBOX MIME-Parsing (Stdlib) | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-3-import-imap.md b/features/PROJ-3-import-imap.md new file mode 100644 index 0000000..e10ac7f --- /dev/null +++ b/features/PROJ-3-import-imap.md @@ -0,0 +1,205 @@ +# PROJ-3: E-Mail-Import: IMAP-Verbindung + +## Status: In Review +**Created:** 2026-03-12 +**Last Updated:** 2026-03-14 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – nur Admins verwalten IMAP-Verbindungen +- Requires: PROJ-5 (Speicherung & Indexierung) – importierte E-Mails werden gespeichert + +## User Stories +- Als Admin oder User möchte ich einen IMAP-Server konfigurieren (Host, Port, Zugangsdaten), damit das System E-Mails von dort abholen kann. +- Als Admin oder User möchte ich beim ersten Verbinden alle vorhandenen E-Mails eines Postfachs importieren (Initial-Import). +- Als System möchte ich beim Verbinden automatisch Junk- und Trash-Ordner per IMAP erkennen und ausschließen, damit kein Spam ins Archiv gelangt. Alle anderen Ordner werden importiert. +- Als Admin oder User möchte ich den Verbindungsstatus sehen (verbunden, Fehler, letzter Sync), damit ich Probleme erkennen kann. +- Als System möchte ich die IMAP-Verbindung testen bevor sie gespeichert wird, damit Konfigurationsfehler früh erkannt werden. +- Als Admin möchte ich alle IMAP-Konten aller Nutzer sehen und verwalten können. +- Als User möchte ich nur meine eigenen IMAP-Konten sehen und verwalten (keine fremden). + +## Acceptance Criteria +- [ ] Konfigurationsformular: Host, Port, TLS/SSL, Benutzername, Passwort +- [ ] Verbindungstest vor dem Speichern (Timeout: 10 Sekunden) +- [ ] Passwörter werden verschlüsselt in der Datenbank gespeichert (nie im Klartext) +- [ ] Automatische Ordner-Erkennung via IMAP LIST-EXTENDED (RFC 6154 Special-Use Flags: `\Junk`, `\Trash`) +- [ ] Fallback auf bekannte Ordnernamen wenn Special-Use Flags fehlen: `Junk`, `Spam`, `Trash`, `Deleted Items`, `Deleted Messages`, `Papierkorb` +- [ ] Erkannte Ausschluss-Ordner werden dem Admin vor dem Import angezeigt (mit Option zur manuellen Korrektur) +- [ ] Initial-Import: **alle Ordner außer Junk/Trash** – Ordnerstruktur wird verworfen, nur E-Mail-Inhalt archiviert +- [ ] Fortschrittsanzeige während Initial-Import +- [ ] Duplikate (Message-ID) werden übersprungen +- [ ] Verbindungsstatus-Übersicht im Admin-Bereich + +## Edge Cases +- IMAP-Server nicht erreichbar → Fehlermeldung mit Retry-Option +- Falsche Zugangsdaten → klare Fehlermeldung +- IMAP-Server trennt Verbindung während Import → automatischer Reconnect +- Postfach mit 200.000+ E-Mails → paginierter Import, kein Speicher-Overflow +- OAuth2/XOAUTH2 für Gmail/Outlook → als spätere Erweiterung markiert (nicht MVP) + +## Technical Requirements +- **Verbindungsmodi:** + - `SSL/TLS` – direkte TLS-Verbindung ab dem ersten Byte (Port 993) + - `STARTTLS` – Verbindung startet unverschlüsselt, wird per STARTTLS-Befehl auf TLS hochgestuft (Port 143) + - `None` – unverschlüsselt, nur für lokale/Testumgebungen +- IMAP IDLE-Unterstützung für Echtzeit-Benachrichtigungen (optional) +- Zugangsdaten AES-256-GCM verschlüsselt in der DB (gleicher Key wie Mail-Store) + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend (Admin-Bereich):** +``` +/admin/imap +├── IMAP-Verbindungsliste +│ └── VerbindungsCard (pro Konto) +│ ├── Name, Host, Status (OK / Fehler) +│ ├── Letzter Import + Anzahl importierter Mails +│ └── Aktionen: Bearbeiten / Löschen / Import starten +├── Verbindung-Formular (anlegen / bearbeiten) +│ ├── Host, Port, TLS-Auswahl +│ ├── Benutzername, Passwort +│ └── [Verbindung testen] Button +├── Ordner-Vorschau (nach erfolgreichem Test) +│ ├── Automatisch erkannte Ausschlüsse (Junk/Trash) – markiert +│ └── Manuelle Korrektur möglich (Checkbox pro Ordner) +└── Import-Fortschrittsanzeige + ├── Fortschrittsbalken (X von Y E-Mails) + ├── Aktueller Status + └── Abschlussbericht (importiert / übersprungen / Fehler) +``` + +**Go Backend:** +``` +IMAP-Dienst +├── Verbindungsverwaltung +│ ├── POST /api/admin/imap ← Verbindung anlegen +│ ├── POST /api/admin/imap/test ← testen + Ordner-Erkennung +│ ├── GET /api/admin/imap ← alle Verbindungen auflisten +│ └── DELETE /api/admin/imap/{id} ← Verbindung löschen +│ +├── IMAP-Client +│ ├── TLS/SSL + STARTTLS Handler +│ ├── Ordner-Erkenner (Special-Use + Fallback) +│ ├── SELECT + FETCH UID-basiert +│ └── Reconnect-Handler +│ +├── Import-Worker (Hintergrund-Goroutine) +│ ├── Batch-weise FETCH (50 Mails pro Batch) +│ ├── Duplikat-Check (Message-ID) +│ ├── → Storage Coordinator (PROJ-5) +│ └── Fortschritt in DB schreiben +│ +└── GET /api/admin/imap/{id}/progress ← Polling durch Frontend +``` + +### Ordner-Erkennungslogik + +``` +IMAP-Verbindung aufgebaut + │ + ▼ +LIST-EXTENDED "" "*" RETURN (SPECIAL-USE) ← RFC 6154 + │ + ├── \Junk gefunden? → Ordner ausschließen + ├── \Trash gefunden? → Ordner ausschließen + └── Flags nicht unterstützt? → Fallback: + Ordnernamen prüfen (case-insensitive): + "junk", "spam", "trash", "deleted items", + "deleted messages", "papierkorb" + → übereinstimmende Ordner ausschließen + │ + ▼ +Ordnerliste mit Markierungen an Frontend: + INBOX ✓ (wird importiert) + Sent ✓ (wird importiert) + Drafts ✓ (wird importiert) + Junk [\Junk erkannt] ✗ (ausgeschlossen) + Trash [\ Trash erkannt] ✗ (ausgeschlossen) + │ + ▼ +Admin kann Ausschlüsse manuell korrigieren +→ Speichern → Import starten +``` + +### Importfluss + +``` +Import-Worker startet + │ + ▼ +Für jeden nicht ausgeschlossenen Ordner: + │ + ├── IMAP UID SEARCH ALL → alle UIDs + │ + └── Batch-weise (50 UIDs): + ├── IMAP FETCH RFC822 + ├── Message-ID Duplikat? → überspringen + └── Storage Coordinator (PROJ-5) + → verschlüsseln + speichern + indexieren + │ + ▼ +Fortschritt in DB → Frontend pollt alle 2 Sek. +``` + +### Datenmodell + +**Tabelle `imap_accounts`:** + +| Feld | Beschreibung | +|------|-------------| +| `id` | Interne ID | +| `name` | Bezeichnung | +| `host` | IMAP-Hostname | +| `port` | Port (143 / 993) | +| `tls` | `ssl` / `starttls` / `none` | +| `username` | IMAP-Benutzername | +| `password_enc` | AES-256-GCM verschlüsseltes Passwort | +| `excluded_folders` | JSON-Array ausgeschlossener Ordner | +| `last_import_at` | Zeitpunkt des letzten Imports | +| `last_import_count` | Anzahl importierter Mails | +| `status` | `idle` / `running` / `error` | +| `error_msg` | Letzter Fehler | + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **RFC 6154 Special-Use Flags** | Standard-Weg für Junk/Trash-Erkennung – funktioniert bei Dovecot, Exchange, Gmail | +| **Fallback auf Ordnernamen** | Ältere oder nicht-standardkonforme Server kennen Special-Use nicht – Fallback deckt die gängigsten Namen ab | +| **Admin-Korrektur möglich** | Automatik kann irren – Admin sieht die Erkennungsergebnisse und kann vor dem Import eingreifen | +| **Nur Ausschlüsse konfigurieren** | Einfacher als Whitelist: alle Ordner importieren außer den erkannten Ausreißern | +| **UID-basierter Fetch** | Bei Reconnect kann genau dort weitergemacht werden wo abgebrochen wurde | +| **Batch-Größe 50** | Balance zwischen RAM-Verbrauch und IMAP-Roundtrips | + +### Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `github.com/emersion/go-imap` | IMAP-Client (RFC 6154 LIST-EXTENDED, TLS, FETCH) | + +## Implementation Notes (2026-03-14) + +### Go Backend +- **`internal/imap/store.go`**: DB CRUD for `imap_accounts` table with AES-256-GCM password encryption. Auto-migrates table on startup. Index on `owner` column. +- **`internal/imap/client.go`**: IMAP client wrapper using `go-imap/v2` (beta.8). Supports SSL/STARTTLS/plaintext. Folder detection via RFC 6154 special-use flags with name-based fallback. +- **`internal/imap/importer.go`**: Background import worker. Fetches all UIDs per folder, processes in batches of 50, stores via `storage.Store.Save()` (SHA256 dedup), indexes via `index.Indexer.IndexSync()`. Progress written to DB for frontend polling. +- **`internal/api/server.go`**: 6 new IMAP endpoints (`GET/POST /api/imap`, `DELETE /api/imap/{id}`, `POST /api/imap/test`, `POST /api/imap/{id}/import`, `GET /api/imap/{id}/progress`). All auth-protected, ownership enforced (admin sees all, user sees own). +- **`cmd/archivmail/main.go`**: Wires IMAP store and importer into API server. + +### Frontend +- **`src/app/imap/page.tsx`**: Full IMAP management page with account cards, add dialog (with connection test and folder preview), progress polling, delete confirmation. +- **`src/lib/api.ts`**: IMAP types and 6 API functions. +- **`src/components/navbar.tsx`**: Added "IMAP Import" link for all roles. + +### Deviations from spec +- Routes use `/api/imap` (not `/api/admin/imap`) since all authenticated users can manage their own IMAP accounts. +- Using `go-imap/v2` beta.8 (latest available) instead of beta.5. +- IMAP page at `/imap` (not `/admin/imap`) to match the route pattern. + +## QA Test Results +_To be added by /qa_ + +## Deployment +Deployed to 192.168.1.131 on 2026-03-14. Both `archivmail` and `archivmail-web` services restarted and active. Database table `imap_accounts` auto-created with index. diff --git a/features/PROJ-4-import-smtp.md b/features/PROJ-4-import-smtp.md new file mode 100644 index 0000000..c377e5c --- /dev/null +++ b/features/PROJ-4-import-smtp.md @@ -0,0 +1,164 @@ +# PROJ-4: E-Mail-Import: SMTP-Eingang (primär via BCC) + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Hinweis +**Dies ist der primäre Eingangsweg.** archivmail enthält einen eingebetteten SMTP-Daemon, der **ausschließlich E-Mails empfängt** – kein Versand, keine Weiterleitung, kein MTA. Postfix (oder ein anderer Mailserver) wird per BCC-Mapping oder Always-BCC-Regel so konfiguriert, dass er eine Kopie jeder E-Mail an archivmails SMTP-Daemon zustellt. + +``` +Absender → Postfix (MTA) → Empfänger + │ + └── BCC/always_bcc → archivmail SMTP-Daemon (nur Empfang) + │ + ▼ + Storage Coordinator +``` + +IMAP und EML/MBOX-Upload sind sekundäre/ergänzende Methoden (z.B. für Altbestände). + +## Dependencies +- Requires: PROJ-5 (Speicherung & Indexierung) – eingehende E-Mails werden gespeichert +- Kein Login nötig für den Empfang – SMTP-Eingang läuft unabhängig vom HTTP-Server + +## User Stories +- Als Mailserver möchte ich E-Mails per BCC an archivmail zustellen, damit diese automatisch archiviert werden. +- Als Admin möchte ich den eingebetteten SMTP-Server konfigurieren (Port, TLS, erlaubte Absender-IPs). +- Als Admin möchte ich festlegen, welche Absender-IPs/Domains akzeptiert werden, damit nur der eigene Mailserver zustellen darf. +- Als System möchte ich eingehende E-Mails sofort nach Empfang indexieren, damit sie innerhalb von Sekunden durchsuchbar sind. +- Als Admin möchte ich den Status des SMTP-Empfängers sehen (läuft, Port, letzte empfangene E-Mail). + +## Acceptance Criteria +- [ ] Eingebetteter SMTP-Server lauscht auf konfigurierbarem Port (Standard: 25 oder 2525) +- [ ] TLS/STARTTLS-Unterstützung für verschlüsselte Übertragung +- [ ] IP-Allowlist: nur eingetragene Mailserver-IPs dürfen zustellen (Standard: nur localhost/127.0.0.1) +- [ ] Optionale Domain-Allowlist als zusätzliche Prüfebene +- [ ] E-Mails werden sofort nach Empfang gespeichert und indexiert +- [ ] SMTP-Quittierung (250 OK) erst nach erfolgreicher Speicherung +- [ ] Admin-UI zeigt: Port, TLS-Status, Anzahl empfangener E-Mails, letzte Aktivität +- [ ] Fehlerhafte/abgelehnte E-Mails werden geloggt + +## Edge Cases +- E-Mail ohne Absender (Envelope-From leer) → annehmen aber markieren +- Sehr große E-Mail (> 50 MB) → konfigurierbare Maximalgröße, Ablehnung mit 552-Fehlercode +- SMTP-Server-Port bereits belegt → klare Fehlermeldung beim Start +- Parallele Verbindungen (viele E-Mails gleichzeitig) → Connection-Pooling +- Duplicate Message-ID → überspringen wie bei anderen Import-Methoden + +## Technical Requirements +- RFC 5321 (SMTP) konformer **reiner Empfänger** – kein SMTP-Versand, keine Queue, kein Relay +- Kein SMTP AUTH – Zugang ausschließlich über IP-Allowlist (nur Postfix-IP eingetragen) +- Maximale Nachrichtengröße konfigurierbar (Standard: 50 MB) +- Startet als eigenständiger Goroutine/Service neben dem HTTP-Server +- Postfix-Konfiguration (außerhalb von archivmail, Dokumentation in README): + - `always_bcc = archiv@archivmail-host` in Postfix `main.cf`, oder + - Sender/Recipient BCC-Maps für granulare Kontrolle + +--- +## Tech Design (Solution Architect) + +### Systemübersicht + +``` +Absender → Postfix (MTA) → Empfänger (normale Zustellung) + │ + └── always_bcc / BCC-Map + │ + ▼ SMTP (Port 2525) + archivmail SMTP-Daemon + (nur Empfang, kein Versand) + │ + ▼ + Storage Coordinator (PROJ-5) + (speichern + indexieren) +``` + +### Komponentenstruktur + +``` +archivmail (Go-Binary) +│ +├── HTTP-Server (Web-GUI + API) +│ +└── SMTP-Daemon ← startet parallel zum HTTP-Server + ├── TCP Acceptor ← lauscht auf Port 2525 (konfigurierbar) + ├── IP Allowlist Guard ← prüft Absender-IP vor SMTP-Dialog + ├── Session Handler (pro Verbindung, eigene Goroutine) + │ ├── TLS/STARTTLS Handler ← optional, Zertifikat aus config.yml + │ └── Size Limiter ← bricht DATA-Phase bei Überschreitung ab + └── Handoff → Storage Coordinator ← übergibt E-Mail nach vollständigem Empfang +``` + +### SMTP-Dialogfluss + +``` +Postfix + │ TCP-Verbindung auf Port 2525 + ▼ +IP Allowlist Guard + ├─ IP unbekannt → Verbindung trennen (kein SMTP-Dialog) + └─ IP erlaubt → weiter + │ + ▼ +220 archivmail SMTP ready + │ +EHLO mail.firma.de +250 OK (kein AUTH angeboten – reiner Empfänger) + │ +MAIL FROM: +250 OK + │ +RCPT TO: +250 OK + │ +DATA +354 Start input + … E-Mail-Inhalt … (max. 50 MB) +. + │ + ├─ Zu groß → 552 Message size exceeds limit + ├─ Duplikat (Message-ID) → 250 OK (still, kein Fehler – Postfix soll nicht retrying) + └─ Neu → Storage Coordinator → verschlüsselt speichern + indexieren + │ + ▼ +250 OK ← erst nach erfolgreicher Speicherung + │ +QUIT +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Reiner Empfänger, kein MTA** | archivmail ist kein Mailserver – keine ausgehende Queue, kein Relay-Risiko, kein Open-Relay | +| **Kein SMTP AUTH** | Vertrauen basiert auf IP, nicht auf Passwort – Postfix und archivmail laufen im gleichen Netz | +| **250 OK bei Duplikat** | Postfix würde bei Fehler die Mail in die Retry-Queue stellen – sinnlos, da Duplikat bereits archiviert | +| **250 OK erst nach Speicherung** | Solange Postfix keine Bestätigung hat, behält er die Mail und versucht erneut – kein Datenverlust | +| **Port 2525** | Port 25 erfordert root-Rechte; 2525 läuft als unprivilegierter `archivmail`-Systembenutzer | +| **Eine Goroutine pro Session** | Viele parallele Verbindungen ohne Blocking; jede Session ist isoliert | + +### Postfix-Konfiguration (Dokumentation, außerhalb von archivmail) + +``` +# /etc/postfix/main.cf – einfachste Variante (alle Mails) +always_bcc = archiv@archivmail-host + +# Oder granular per Sender-BCC-Map: +# sender_bcc_maps = hash:/etc/postfix/sender_bcc +# empfänger@firma.de archiv@archivmail-host +``` + +### Go-Abhängigkeiten + +| Paket | Zweck | +|---|---| +| `github.com/emersion/go-smtp` | Eingebetteter SMTP-Daemon (RFC 5321, nur Empfang) | +| `crypto/tls` | TLS/STARTTLS (Go Stdlib) | +| `net` | IP-Prüfung (Go Stdlib) | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-5-speicherung-und-indexierung.md b/features/PROJ-5-speicherung-und-indexierung.md new file mode 100644 index 0000000..b1a7d28 --- /dev/null +++ b/features/PROJ-5-speicherung-und-indexierung.md @@ -0,0 +1,194 @@ +# PROJ-5: E-Mail-Speicherung & Volltext-Indexierung + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- None (Basis-Feature, wird von Import-Features genutzt) + +## User Stories +- Als System möchte ich E-Mails unveränderlich (immutable) speichern, damit die Archivintegrität gewährleistet ist. +- Als System möchte ich E-Mail-Inhalte (Betreff, Absender, Empfänger, Body, Anhang-Namen) volltext-indexieren, damit schnelle Suche möglich ist. +- Als Admin möchte ich den Speicherverbrauch einsehen können, damit ich die Kapazität planen kann. +- Als System möchte ich Anhänge getrennt vom E-Mail-Body speichern, damit der Speicher effizient genutzt wird. + +## Acceptance Criteria +- [ ] Jede E-Mail wird mit ihrer originalen MIME-Struktur gespeichert (kein Datenverlust) +- [ ] Metadaten in PostgreSQL: `message_id`, `from`, `to`, `cc`, `subject`, `date`, `size` (Bytes), Attachment-Infos (Dateiname, MIME-Type, Größe, Hash) +- [ ] Kein E-Mail-Body in der DB – Body liegt ausschließlich in `/var/archivmail/store/` verschlüsselt auf Disk +- [ ] Volltext-Index umfasst: Betreff, Absender, Empfänger, CC, BCC, Plain-Text-Body +- [ ] Anhang-Dateinamen und MIME-Types werden indexiert (Inhalt von Anhängen optional) +- [ ] Deduplizierung: Gleiche Message-ID wird nur einmal gespeichert +- [ ] SHA-256-Hash des originalen RFC-2822-Inhalts für Integritätsprüfung gespeichert +- [ ] Admin-Dashboard zeigt: Gesamtanzahl E-Mails, Speicherverbrauch (Store + Astore) +- [ ] Mailkörper gespeichert unter `/var/archivmail/store////xxxxx.m` (AES-256-GCM verschlüsselt) +- [ ] Anhänge gespeichert unter `/var/archivmail/astore/` (AES-256-GCM verschlüsselt) +- [ ] Anhänge werden dedupliziert: gleicher Hash → eine Datei, mehrere Referenzen in der DB +- [ ] Verschlüsselungsschlüssel wird beim Start aus `/etc/archivmail/keyfile` geladen (Pfad konfigurierbar) +- [ ] Key-Datei: `chmod 400`, Owner `archivmail`-Systembenutzer +- [ ] Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Schlüssel ≠ 32 Byte nach Base64-Dekodierung +- [ ] Xapian-Index enthält keinen vollständigen E-Mail-Body (nur Terme/Tokens) +- [ ] PostgreSQL speichert ausschließlich Metadaten + Dateipfade – kein E-Mail-Body in der DB + +## Edge Cases +- E-Mail ohne Body (nur Anhang) → Body als leer speichern, Anhang indexieren +- HTML-Body ohne Plain-Text-Alternative → HTML zu Plain-Text konvertieren für Index +- E-Mail mit sehr vielen Empfängern (> 500) → TO/CC/BCC werden vollständig gespeichert +- Sonderzeichen und Nicht-ASCII in Headern (RFC 2047 encoded) → dekodieren +- Anhang-Deduplizierung: gleicher Inhalt in 1000 E-Mails → nur eine Datei in `astore/`, DB zählt Referenzen; Löschen einer E-Mail dekrementiert Referenzzähler, Datei erst bei 0 gelöscht +- Speicherplatz voll → Import-Fehler mit klarer Meldung, keine partiellen Einträge +- Verschlüsselungsschlüssel fehlt beim Start → Server startet nicht, klare Fehlermeldung +- Schlüssel-Rotation: alte `.enc`-Dateien müssen mit neuem Schlüssel re-verschlüsselt werden (Admin-Tool, nicht automatisch) + +## Technical Requirements +- **Volltext-Index: Xapian** (via CGo-Bindings, z.B. `github.com/rcaught/go-xapian` oder direkte CGo-Integration) + - Xapian-Datenbank liegt auf dem Dateisystem (kein externer Dienst nötig) + - Felder als Xapian-Terms und -Values indexiert: Subject, From, To, CC, BCC, Body + - Stemming für Deutsch und Englisch (Xapian Snowball Stemmer) + - Anhang-Dateinamen als zusätzliche Terms indexiert +- **Speicherung: Verschlüsselt im Dateisystem (AES-256-GCM)** + - Mailkörper (ohne Anhänge) als `.m`-Datei: + ``` + /var/archivmail/store////xxxxx.m + ``` + - Anhänge dedupliziert in separatem Store (ein Anhang = eine Datei, unabhängig wie oft er vorkommt): + ``` + /var/archivmail/astore/ + ``` + - Hash = SHA-256 des Inhalts → dient gleichzeitig als Pfad und Integritätsprüfung + - Beide Stores AES-256-GCM verschlüsselt auf Disk + - Verschlüsselungsschlüssel (32 Byte) aus dedizierter Key-Datei: `/etc/archivmail/keyfile` + - Dateiformat: Base64-kodierter 32-Byte-Schlüssel, eine Zeile + - Dateiberechtigungen: `chmod 400`, Owner: `archivmail` (Systembenutzer des Dienstes) + - Pfad zur Key-Datei konfigurierbar in `config.yml` (`encryption.keyfile`) + - Schlüssel wird beim Start einmalig in den Prozessspeicher geladen – danach keine Disk-Zugriffe mehr auf die Key-Datei + - Server verweigert Start wenn Key-Datei fehlt, nicht lesbar oder Inhalt nicht exakt 32 Byte (nach Base64-Dekodierung) + - PostgreSQL speichert folgende Metadaten (kein Mail-Body): + - `message_id`, `from`, `to`, `cc`, `subject`, `date`, `size` + - Attachment-Tabelle: `filename`, `mime_type`, `size`, `hash` (→ Pfad in `astore/`) + - Pfadreferenz zur `.m`-Datei in `store/` + - Xapian-Datenbank liegt unverschlüsselt auf Disk (enthält nur Text-Terme, keinen vollständigen Body) +- Xapian-Schreibzugriffe serialisiert (WritableDatabase nicht thread-safe) – Background-Worker-Queue +- Indexierung innerhalb 5 Sekunden nach E-Mail-Eingang +- Retention-Policy: konfigurierbare automatische Löschung alter E-Mails (DSGVO) löscht sowohl DB-Eintrag als auch Xapian-Dokument + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +``` +archivmail (Go-Binary) +│ +├── Storage Coordinator ← Einziger Eintrittspunkt für alle Schreibvorgänge +│ ├── MIME Parser ← Zerlegt eingehende E-Mail in Body + Anhänge +│ ├── Mail Store ← Schreibt .m-Datei verschlüsselt auf Disk +│ │ └── Encryption Layer ← AES-256-GCM (Schlüssel aus /etc/archivmail/keyfile) +│ ├── Attachment Store ← Schreibt Anhänge in astore/, prüft Duplikate per Hash +│ │ └── Encryption Layer ← gleiche AES-256-GCM Instanz +│ └── Metadata Writer ← Schreibt Metadaten in PostgreSQL +│ +├── Index Worker (Hintergrund) ← Serialisierte Warteschlange für Xapian-Schreibzugriffe +│ ├── Text Extractor ← HTML → Plain-Text, RFC 2047 Header-Dekodierung +│ └── Xapian WritableDatabase ← Ein Schreiber gleichzeitig (Queue verhindert Konflikte) +│ +└── Xapian ReadonlyDatabase ← Beliebig viele parallele Lesezugriffe (Suche) +``` + +### Datenfluss: E-Mail eingehend + +``` +E-Mail (RFC 2822) – primär via SMTP-BCC + │ + ▼ + MIME Parser + ┌────┴──────────────────────────────────┐ + │ │ + ▼ ▼ +Body (ohne Anhänge) Anhänge (0..n) + │ │ + ├─ SHA-256(Body) → Hash ├─ SHA-256(Anhang) → Hash + ├─ AES-256-GCM verschlüsseln ├─ Hash in astore/ vorhanden? → nur ref_count++ + └─ /var/archivmail/store/ ├─ AES-256-GCM verschlüsseln + ///x.m └─ /var/archivmail/astore/ + + ref_count++ in DB + │ │ + └──────────────┬────────────────────────┘ + ▼ + PostgreSQL (Metadaten) + message_id, from, to, cc, + subject, date, size, + store_path, sha256, + indexed_at = NULL + │ + ▼ + Index Worker Queue (Channel) + │ + ▼ + Text Extractor + (HTML→Text, Encoding-Normalisierung) + │ + ▼ + Xapian WritableDatabase + Subject, From, To, CC, Body als Terms + indexed_at = NOW() in PostgreSQL +``` + +### Datenmodell + +**Tabelle `emails`** – eine Zeile pro archivierter E-Mail: + +| Feld | Beschreibung | +|------|-------------| +| `message_id` | RFC-2822 Message-ID (Primärschlüssel, Duplikatschutz) | +| `from` | Absender | +| `to` | Empfänger | +| `cc` | CC-Empfänger | +| `subject` | Betreff | +| `date` | Sendedatum (UTC) | +| `size` | Größe des Originals in Bytes | +| `store_path` | Pfad zur .m-Datei | +| `sha256` | Hash des Originals (Integritätsprüfung) | +| `indexed_at` | Zeitpunkt der Xapian-Indexierung (NULL = ausstehend) | + +**Tabelle `attachments`** – ein Eintrag pro einzigartigem Anhang: + +| Feld | Beschreibung | +|------|-------------| +| `hash` | SHA-256 des Inhalts (= Dateiname in astore/) | +| `filename` | Originaldateiname | +| `mime_type` | z.B. application/pdf | +| `size` | Größe in Bytes | +| `ref_count` | Anzahl E-Mails die diesen Anhang referenzieren | + +**Tabelle `email_attachments`** – Verknüpfung E-Mail ↔ Anhang (n:m) + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| Body und Anhänge getrennt | Anhang-Deduplizierung: gleicher PDF in 1000 Mails = eine Datei auf Disk | +| SHA-256 als Dateipfad | Hash dient gleichzeitig als Pfad und Integritätsprüfung – kein separates Mapping | +| AES-256-GCM | Authentifizierte Verschlüsselung erkennt Dateimanipulationen (Tamper Detection) | +| Index Worker Queue | Xapian erlaubt nur einen Schreiber – Queue serialisiert ohne Datenverlust | +| `indexed_at` NULL-Flag | Nach Absturz können nicht-indexierte Mails beim Neustart nachindexiert werden | +| Metadaten in PostgreSQL, Body auf Disk | Filterabfragen (Datum, Absender) ohne Disk-Zugriff; Body nur bei Bedarf lesen | +| Storage Coordinator als Single Entry Point | Alle Importwege (SMTP, IMAP, EML/MBOX) rufen dieselbe Schreiblogik auf | + +### Go-Abhängigkeiten + +| Paket | Zweck | +|---|---| +| Xapian CGo-Bindings | Volltext-Index | +| `pgx` | PostgreSQL-Treiber | +| `crypto/aes`, `crypto/cipher` | AES-256-GCM (Go Stdlib) | +| `crypto/sha256` | Hashing (Go Stdlib) | +| `mime`, `mime/multipart` | MIME-Parsing (Go Stdlib) | +| `golang.org/x/net/html` | HTML → Plain-Text für Index | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-6-volltext-suche.md b/features/PROJ-6-volltext-suche.md new file mode 100644 index 0000000..e1708e1 --- /dev/null +++ b/features/PROJ-6-volltext-suche.md @@ -0,0 +1,128 @@ +# PROJ-6: Volltext-Suche & Filterung + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – Suche nur für eingeloggte Nutzer +- Requires: PROJ-5 (Speicherung & Indexierung) – Suchergebnisse kommen aus dem Index + +## User Stories +- Als Nutzer möchte ich nach Schlüsselwörtern suchen, damit ich relevante E-Mails schnell finden kann. +- Als Nutzer möchte ich Suchergebnisse nach Absender, Empfänger, Datum und Anhang filtern, damit ich die Treffermenge eingrenzen kann. +- Als Nutzer möchte ich Suchergebnisse nach Datum sortieren können (neueste/älteste zuerst). +- Als Nutzer möchte ich Suchbegriffe in den Ergebnissen hervorgehoben sehen (Highlighting). +- Als Nutzer sehe ich nur E-Mails, auf die ich Zugriffsrecht habe, damit Datenschutz gewahrt bleibt. + +## Acceptance Criteria +- [ ] Sucheingabe mit Echtzeit-Vorschau (oder sofortiger Submit) +- [ ] Suche in: Betreff, Absender, Empfänger, Body-Text +- [ ] Filteroptionen: Datum (von–bis), Absender-Domain, hat Anhang (ja/nein), Label +- [ ] Sortierung: nach Relevanz, nach Datum (auf-/absteigend) +- [ ] Suchergebnisse paginiert (Standard: 25 pro Seite) +- [ ] Suchbegriff in Betreff und Body-Snippet hervorgehoben +- [ ] Suchanfragen liefern Ergebnisse in < 2 Sekunden (bei 100.000+ E-Mails) +- [ ] Nutzer sehen nur E-Mails in ihren zugewiesenen Postfächern + +## Edge Cases +- Suchanfrage ohne Ergebnisse → "Keine Ergebnisse" Meldung mit Vorschlägen +- Sonderzeichen in Suchanfrage (", *, ?) → Escaping oder Query-Syntax erlauben +- Suche bei sehr großem Index (1M+ Mails) → Performance-Test erforderlich +- Gleichzeitige Suchanfragen von vielen Nutzern → kein Query-Blocking + +## Technical Requirements +- **Such-Engine: Xapian** – die Web-GUI sucht ausschließlich über den Xapian-Index + - Kein SQL-Fulltext-Query gegen PostgreSQL – DB wird nur für Metadaten-Lookup nach Treffern genutzt + - Suchfluss: Web-GUI → API → Xapian-Query → Treffer-IDs → PostgreSQL-Metadaten-Lookup → Antwort + - Xapian QueryParser: AND, OR, NOT, Phrasen (`"exakter Text"`), Wildcards (`word*`), Feldpräfixe (`from:`, `subject:`) + - Relevanz-Ranking über Xapian BM25Weight + - Snippet/Highlighting über `Xapian::MSet::snippet()` + - `ReadonlyDatabase` für parallele Lesezugriffe (mehrere Nutzer gleichzeitig möglich) +- Antwortzeit < 2 Sekunden für Volltext-Suche über 100.000 E-Mails +- Suchanfragen werden für Audit-Log erfasst (optional, konfigurierbar) + +--- +## Tech Design (Solution Architect) + +### Komponentenstruktur + +**Next.js Frontend:** +``` +/search +├── SearchBar ← Eingabefeld, Submit bei Enter oder Button +├── FilterPanel ← aufklappbar +│ ├── DateRangePicker ← von–bis Datum +│ ├── DomainFilter ← Absender-Domain Freitext +│ ├── AttachmentToggle ← nur Mails mit Anhang +│ └── LabelFilter ← Label-Auswahl (Mehrfachauswahl) +├── SortControls ← Relevanz / Datum aufsteigend / absteigend +├── ResultsList +│ └── MailCard (pro Treffer) +│ ├── Betreff (Suchbegriff hervorgehoben) +│ ├── Von / An / Datum / Größe +│ ├── Body-Snippet (Suchbegriff hervorgehoben) +│ └── Anhang-Indikator +├── Pagination ← Seiten-Navigation +└── EmptyState ← "Keine Ergebnisse" mit Suchtipps +``` + +**Go Backend:** +``` +GET /api/search +├── Session Middleware ← Auth prüfen +├── Role Filter Builder ← user: nur eigene Postfächer / auditor: alle +├── Xapian QueryParser ← Nutzer-Query parsen (AND/OR/NOT/Wildcards) +├── Xapian ReadonlyDatabase ← Query ausführen, MSet zurückgeben +│ ├── BM25 Relevanz-Ranking ← beste Treffer zuerst +│ └── MSet::snippet() ← Highlighting-Snippets erzeugen +├── PostgreSQL Metadaten-Lookup ← From, To, Subject, Date, Size, Attachments +└── JSON Response Assembly ← Ergebnis zusammenbauen +``` + +### Suchfluss + +``` +Next.js (Browser) Go Backend + │ │ + │ GET /api/search │ + │ ?q=Rechnung&date_from=2024-01 │ + │ &has_attachments=true&page=2 │ + │ ────────────────────────────────► │ + │ Session prüfen + │ Rolle ermitteln: + │ user → Filter: nur eigene Postfach-IDs + │ auditor → kein Filter + │ │ + │ Xapian QueryParser + │ → Query + Datumsfilter + Anhang-Filter + │ │ + │ Xapian ReadonlyDatabase + │ → MSet: [doc_id_1, doc_id_5, ...] + │ → Snippets mit Highlighting + │ │ + │ PostgreSQL + │ → Metadaten für doc_ids laden + │ │ + │ ◄──────────────────────────────── + │ { total, page, mails: [...] } │ + │ MailCards rendern + highlighten │ +``` + +### Technische Entscheidungen + +| Entscheidung | Begründung | +|---|---| +| **Xapian für alles, kein SQL-Fulltext** | Optimiert für Volltext – PostgreSQL LIKE-Suche wäre bei 100k+ Mails zu langsam | +| **ReadonlyDatabase** | Beliebig viele parallele Lesezugriffe – kein Blocking bei gleichzeitigen Nutzern | +| **Rolle als Xapian-Term** | Postfach-ID beim Indexieren als Term gespeichert – Rollenfilter läuft in Xapian, nicht nachträglich in der DB | +| **Snippets aus Xapian** | `MSet::snippet()` hebt Suchbegriff im Originaltext hervor – kein separates Rendering nötig | +| **Paginierung über Xapian Offset** | Nur angefragter Seitenausschnitt zurückgegeben – kein Full-Scan pro Seite | +| **PostgreSQL nur für Metadaten** | Nach Xapian-Suche werden nur gefundene IDs nachgeschlagen – minimale DB-Last | +| **URL-State mit `nuqs`** | Suchparameter in der URL → Back-Button funktioniert, Suchergebnisse sind verlinkbar | + +## QA Test Results +_To be added by /qa_ + +## Deployment +_To be added by /deploy_ diff --git a/features/PROJ-7-email-ansicht.md b/features/PROJ-7-email-ansicht.md new file mode 100644 index 0000000..6102696 --- /dev/null +++ b/features/PROJ-7-email-ansicht.md @@ -0,0 +1,184 @@ +# PROJ-7: E-Mail-Ansicht (Lesen & Anhänge) + +## Status: In Progress +**Created:** 2026-03-12 +**Last Updated:** 2026-03-12 + +## Dependencies +- Requires: PROJ-1 (Authentifizierung) – nur eingeloggte Nutzer mit Zugriffsrecht +- Requires: PROJ-5 (Speicherung & Indexierung) – E-Mail-Daten aus der Datenbank + +## User Stories +- Als Nutzer möchte ich eine E-Mail aus den Suchergebnissen öffnen und lesen, damit ich den vollständigen Inhalt sehe. +- Als Nutzer möchte ich Anhänge herunterladen, damit ich auf angefügte Dokumente zugreifen kann. +- Als Nutzer möchte ich die originalen E-Mail-Header einsehen (technische Details), damit ich Routing und Authentizität prüfen kann. +- Als Nutzer möchte ich E-Mails im HTML-Format sehen (mit sanitizierten externen Inhalten), damit die Formatierung erhalten bleibt. +- Als Nutzer möchte ich die Originalmail als EML herunterladen können. + +## Acceptance Criteria +- [ ] E-Mail-Detailansicht zeigt: Von, An, CC, Datum, Betreff, Body +- [ ] HTML-Body wird **original** dargestellt – kein Entfernen oder Verändern von Inhalten +- [ ] Darstellung in einem vollständig isolierten `