feat(PROJ-18): E-Mail Integritätsprüfung (SHA-256 Verifikation)
- Storage: VerifyIntegrity, GetAllIDs, GetVerifyStatus + DB-Spalten
- main: Hintergrund-Worker alle 5 Minuten (beim Start sofort: 40/40 OK)
- API: verify_ok + verified_at in GET /api/mails/{id} Antwort
- Frontend: Grüner Haken / graues X / rotes X in Mail-Ansicht
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -548,6 +548,17 @@ func (s *Server) handleGetMail(w http.ResponseWriter, r *http.Request) {
|
||||
dateStr = pm.Date.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Verify status
|
||||
vs, _ := s.store.GetVerifyStatus(r.Context(), id)
|
||||
var verifyOK interface{} = nil
|
||||
var verifiedAt interface{} = nil
|
||||
if vs.VerifyOK != nil {
|
||||
verifyOK = *vs.VerifyOK
|
||||
}
|
||||
if vs.VerifiedAt != nil {
|
||||
verifiedAt = vs.VerifiedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"id": id,
|
||||
"from": pm.From,
|
||||
@@ -560,6 +571,8 @@ func (s *Server) handleGetMail(w http.ResponseWriter, r *http.Request) {
|
||||
"body_plain": pm.TextBody,
|
||||
"raw_headers": extractRawHeaders(raw),
|
||||
"attachments": attachments,
|
||||
"verify_ok": verifyOK,
|
||||
"verified_at": verifiedAt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ func New(cfg Config) (*Store, error) {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("storage: init schema: %w", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, _ = s.db.Exec(ctx, `ALTER TABLE emails ADD COLUMN IF NOT EXISTS verify_ok BOOLEAN`)
|
||||
_, _ = s.db.Exec(ctx, `ALTER TABLE emails ADD COLUMN IF NOT EXISTS verified_at TIMESTAMPTZ`)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -578,3 +581,74 @@ func (s *Store) WalkStore(ctx context.Context, fn func(id string) error) error {
|
||||
func (s *Store) filePath(id string) string {
|
||||
return filepath.Join(s.dir, "store", id[:2], id)
|
||||
}
|
||||
|
||||
// ── Integrity verification ────────────────────────────────────────────────
|
||||
|
||||
// VerifyIntegrity re-computes the SHA-256 of the stored plaintext and
|
||||
// compares it to the file ID. Updates verify_ok and verified_at in the DB.
|
||||
func (s *Store) VerifyIntegrity(ctx context.Context, id string) (bool, error) {
|
||||
raw, err := s.Load(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
computed := fmt.Sprintf("%x", sum[:])
|
||||
ok := computed == id
|
||||
if s.db != nil {
|
||||
s.db.Exec(ctx,
|
||||
`UPDATE emails SET verify_ok=$1, verified_at=NOW() WHERE id=$2`,
|
||||
ok, id)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// GetAllIDs returns all email IDs from the DB, or walks the store if no DB.
|
||||
func (s *Store) GetAllIDs(ctx context.Context) ([]string, error) {
|
||||
if s.db != nil {
|
||||
rows, err := s.db.Query(ctx, `SELECT id FROM emails ORDER BY received_at`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
// fallback: walk store
|
||||
var ids []string
|
||||
err := s.WalkStore(ctx, func(id string) error {
|
||||
ids = append(ids, id)
|
||||
return nil
|
||||
})
|
||||
return ids, err
|
||||
}
|
||||
|
||||
// VerifyStatus holds the result of an integrity verification.
|
||||
type VerifyStatus struct {
|
||||
VerifyOK *bool // nil = not yet checked
|
||||
VerifiedAt *time.Time
|
||||
}
|
||||
|
||||
// GetVerifyStatus returns the stored verification status for a given email ID.
|
||||
func (s *Store) GetVerifyStatus(ctx context.Context, id string) (VerifyStatus, error) {
|
||||
var vs VerifyStatus
|
||||
if s.db == nil {
|
||||
return vs, nil
|
||||
}
|
||||
row := s.db.QueryRow(ctx,
|
||||
`SELECT verify_ok, verified_at FROM emails WHERE id=$1`, id)
|
||||
var ok *bool
|
||||
var at *time.Time
|
||||
if err := row.Scan(&ok, &at); err != nil {
|
||||
return vs, nil // not found = not verified
|
||||
}
|
||||
vs.VerifyOK = ok
|
||||
vs.VerifiedAt = at
|
||||
return vs, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user