feat(PROJ-85): Fix undeklarierte 8-Bit-Zeichen in Header/Body ohne Encoded-Word

Getrennt von PROJ-84: Header (v.a. Subject) mit rohen 8-Bit-Bytes ohne
RFC-2047-Encoded-Word-Syntax wurden nicht auf tatsächliches Charset
geprüft, landeten als ungültiges UTF-8 in emails.subject und Manticore.
Gleiche Lücke bei decodeCharset() für den Body ohne verwertbaren
Content-Type.

RepairUTF8/RepairUTF8Bytes (charset_repair.go): bytegenaue Reparatur,
gültiges UTF-8 bleibt Identität, nur ungültige Byte-Sequenzen fallen
auf Windows-1252 zurück. Attachment.Data bewusst ausgenommen (bleibt
byte-exakt für Downloads). fix-subjects-Kommando erkennt jetzt beide
Fälle (HasEncodedWord || NeedsCharsetRepair).

Verifiziert auf 192.168.1.132: 54 zusätzliche Subject-Fälle, 219
Body-Fälle behoben (bodyInvalidUTF8 219 -> 0). --apply noch nicht
ausgeführt, Body-Korrektur braucht zusätzlich reindex.

Die ursprünglich gemeldete Amazon-Mail bleibt bewusst unverändert:
Encoding-Fehler kam bereits so vom Absender (=3F statt =DC im
Original-Encoded-Word), GoBD verbietet nachträgliche Korrektur
archivierter Originalinhalte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
This commit is contained in:
sysops
2026-08-06 20:46:58 +02:00
co-authored by Claude Sonnet 5
parent 786d8341f4
commit 8f46d688a8
10 changed files with 369 additions and 12 deletions
+6 -2
View File
@@ -86,7 +86,7 @@ func runFixSubjects(args []string) {
defer func() { idxMgr.Close() }()
}
rows, err := mailStore.ListRawEncodedSubjects(ctx, tenantPtr, *limitFlag)
rows, err := collectSubjectCandidates(ctx, mailStore, tenantPtr, *limitFlag)
if err != nil {
logger.Error("failed to list candidate subjects", "err", err)
os.Exit(1)
@@ -109,7 +109,11 @@ func runFixSubjects(args []string) {
)
for _, row := range rows {
if !mailparser.HasEncodedWord(row.Subject) {
// Two independent defect classes are repaired here:
// 1. undecoded RFC 2047 encoded-word (PROJ-84)
// 2. raw 8-bit Windows-1252/ISO-8859-1 bytes without encoded-word,
// stored as invalid UTF-8
if !mailparser.HasEncodedWord(row.Subject) && !mailparser.NeedsCharsetRepair(row.Subject) {
continue
}
@@ -0,0 +1,46 @@
package main
import (
"context"
"archivmail/internal/storage"
)
// collectSubjectCandidates merges the two candidate sets the fix-subjects
// backfill has to consider and removes duplicates (a subject can be affected by
// both defects at once):
//
// - ListRawEncodedSubjects: subjects that still contain an undecoded RFC 2047
// encoded-word (`=?charset?B|Q?...?=`) — the PROJ-84 class.
// - ListNonASCIISubjects: subjects with raw 8-bit bytes. A separate query is
// unavoidable because such headers carry no `=?...?=` marker at all, so the
// encoded-word LIKE filter can never match them.
//
// limit <= 0 means no limit; otherwise it caps the merged result so that
// --limit stays a predictable upper bound on inspected mails.
func collectSubjectCandidates(ctx context.Context, store *storage.Store, tenantID *int64, limit int) ([]storage.SubjectRow, error) {
encoded, err := store.ListRawEncodedSubjects(ctx, tenantID, limit)
if err != nil {
return nil, err
}
nonASCII, err := store.ListNonASCIISubjects(ctx, tenantID, limit)
if err != nil {
return nil, err
}
seen := make(map[string]struct{}, len(encoded)+len(nonASCII))
out := make([]storage.SubjectRow, 0, len(encoded)+len(nonASCII))
for _, list := range [][]storage.SubjectRow{encoded, nonASCII} {
for _, r := range list {
if _, dup := seen[r.ID]; dup {
continue
}
seen[r.ID] = struct{}{}
out = append(out, r)
if limit > 0 && len(out) >= limit {
return out, nil
}
}
}
return out, nil
}
+2 -1
View File
@@ -100,7 +100,8 @@
| PROJ-82 | Print-Farbparität zwischen Hell- und Dark-Mode-Ausdrucken | Planned | [PROJ-82](PROJ-82-print-farbparitaet-dark-mode.md) | 2026-08-06 |
| PROJ-83 | Audit-Logging für Anhang-Abrufe (GoBD/DSGVO-Nachbesserung) | Planned | [PROJ-83](PROJ-83-audit-log-anhang-abrufe.md) | 2026-08-06 |
| PROJ-84 | Fix MIME-Header-Charset-Dekodierung + Backfill für Bestandsmails | In Review | [PROJ-84](PROJ-84-fix-mime-header-charset-backfill.md) | 2026-08-06 |
| PROJ-85 | Fix undeklarierte 8-Bit-Zeichen in Header/Body ohne Encoded-Word | In Review | [PROJ-85](PROJ-85-fix-undeklarierte-8bit-header-charset.md) | 2026-08-06 |
<!-- Add features above this line -->
## Next Available ID: PROJ-85
## Next Available ID: PROJ-86
@@ -24,7 +24,7 @@ Fix (Commit 31d7113) behebt das für künftige Importe. Bestandsmails behalten d
- [x] Korrektur ändert ausschließlich die Postgres-Metadaten-Spalte `emails.subject` — die archivierte Original-EML im Storage-Layer bleibt unverändert
- [x] Nach Korrektur wird der Manticore-Suchindex für die betroffene Mail nachgezogen (inkl. Erhalt von vorhandenem OCR-Text)
- [x] Jeder Lauf (auch Dry-Run) erzeugt einen Audit-Log-Eintrag (`event_type=metadata_backfill`) mit Zählern
- [ ] `--apply`-Lauf auf 132 durchgeführt und stichprobenartig verifiziert
- [x] `--apply`-Lauf auf 132 durchgeführt und stichprobenartig verifiziert (541/541 aktualisiert, 0 Fehler, User-Gegenprobe an ARAG-Mail bestätigt 2026-08-06)
- [ ] `--apply`-Lauf auf 131 (Produktiv) durchgeführt, nach Freigabe
## Edge Cases
@@ -0,0 +1,67 @@
# PROJ-85: Fix undeklarierte 8-Bit-Zeichen in Header/Body ohne Encoded-Word
## Status: In Review
**Created:** 2026-08-06
**Last Updated:** 2026-08-06
## Kontext
Anlass war eine gemeldete Mojibake-Mail ("Grü?e" statt "Grüße"). Diagnose ergab: diese konkrete Mail ist korrekt archiviert — der Fehler (`=3F` statt `=DC` im Original-Encoded-Word) kam bereits so vom Absender (Amazon, 2018, DKIM-signiert) und darf laut GoBD nicht nachträglich verändert werden.
Beim Scan wurde daneben aber ein echter archivmail-Bug gefunden, verschieden von PROJ-84: Header (insbesondere Subject) mit rohen 8-Bit-Bytes **ohne** RFC-2047-Encoded-Word-Syntax (technisch RFC-5322-widrig, in freier Wildbahn bei Marketing-/Newsletter-Systemen üblich) wurden nicht auf ihr tatsächliches Charset geprüft. `decodeCharset()` hatte dieselbe Lücke für den Body bei fehlendem/leerem Content-Type. Ergebnis: ungültige UTF-8-Bytes landeten unverändert in `emails.subject` (Postgres, DB-Encoding `SQL_ASCII`, akzeptiert das klaglos) und im Manticore-Index.
## Dependencies
- Ergänzt PROJ-84 (Fix MIME-Header-Charset-Dekodierung) — andere Ursache (kein Encoded-Word-Marker vorhanden), daher eigenes Ticket statt Erweiterung
- Nutzt dieselbe Backfill-Infrastruktur (`fix-subjects`-Kommando, Audit-Log)
## User Stories
- Als User will ich, dass Betreffs mit undeklarierten 8-Bit-Sonderzeichen (ä/ö/ü/ß/„–"/„®" etc.) korrekt lesbar sind, wenn der Absender kein valides Encoded-Word genutzt hat.
- Als User will ich, dass auch der Mail-Body korrekt dargestellt/durchsuchbar ist, wenn das deklarierte oder fehlende Charset nicht zu den tatsächlichen Bytes passt.
- Als Auditor will ich, dass eine Reparatur klar von echter Absenderseiten-Korruption unterschieden wird — Mails, die der Absender bereits fehlerhaft verschickt hat (wie das Encoded-Word-Beispiel), werden NICHT verändert.
## Acceptance Criteria
- [x] `RepairUTF8`/`RepairUTF8Bytes` erkennt Bytes außerhalb gültiger UTF-8-Sequenzen und versucht Windows-1252-Fallback-Dekodierung, bytegenau (nicht bufferweise) — gültiges UTF-8 bleibt unverändert
- [x] `decodeMIMEHeader()` wendet die Reparatur auf das Ergebnis an (inkl. Fehlerpfad)
- [x] Body-Decoding (Single-Part und Multipart, alle Content-Type-Fehlerpfade) wendet dieselbe Reparatur an, außer bei binären Attachments (`Attachment.Data` bleibt byte-exakt)
- [x] `fix-subjects`-Kommando erkennt zusätzlich zu Encoded-Word-Fällen (PROJ-84) auch rohe 8-Bit-Subjects ohne Encoded-Word-Marker (`NeedsCharsetRepair`)
- [x] Tests decken ab: gültiges UTF-8 bleibt unangetastet, Windows-1252-Sonderzeichen (ä/ö/ü/ß//®) werden korrekt repariert
- [ ] `--apply` auf 132 durchgeführt, danach `archivmail reindex` für Body-Korrektur
- [ ] `--apply` + `reindex` auf 131 (Produktiv), Umfang dort separat messen (131 ≠ 132, siehe PROJ-84-Erfahrung: 0 vs. 543 Kandidaten)
## Edge Cases
- Encoded-Word mit RFC-5322-widriger Syntax (Leerzeichen im codierten Teil) → bleibt unverändert, als „undecodable" gezählt, nicht Teil dieses Fixes (schon in PROJ-84 als Grenzfall dokumentiert)
- Mail, bei der der Absender selbst bereits fehlerhaft kodiert hat (z.B. `=3F` statt korrektem Byte im Encoded-Word) → wird NICHT repariert, da das die archivierte Originalnachricht selbst verfälschen würde (GoBD-Grundsatz: Archiv bildet ab, was ankam, nicht was gemeint war)
- Binäre Attachments (z.B. CSV mit Windows-1252) → `Attachment.Data` bleibt byte-exakt, keine Reparatur, sonst wäre der heruntergeladene Anhang verändert
- Text, der zufällig wie gültiges UTF-8 aussieht, aber eigentlich Windows-1252 ist → nicht erkennbar/nicht behandelt (Kollisionsfall, in der Praxis selten und nicht zuverlässig unterscheidbar)
## Technical Requirements (optional)
- Kein neues Datenbankschema
- Postgres-DB-Encoding bleibt `SQL_ASCII` (Bestand, nicht Teil dieses Fixes) — Nebenbefund, der erklärt, warum ungültige Bytes bisher klaglos angenommen wurden
- Body-Korrektur wirkt erst nach zusätzlichem `archivmail reindex`-Lauf (Body liegt nur in Manticore, nicht in Postgres)
---
<!-- Sections below are added by subsequent skills -->
## Tech Design (Solution Architect)
Direkt umgesetzt ohne vorgelagerte Architektur-Phase — Bugfix + Backfill-Erweiterung auf bestehender Infrastruktur (PROJ-84), kein neues UI, kein neues Datenmodell.
## Implementation Notes (Backend, 2026-08-06)
Neue/geänderte Dateien:
- `pkg/mailparser/charset_repair.go` (neu) — `RepairUTF8()`, `RepairUTF8Bytes()`, `NeedsCharsetRepair()`. Strategie: erst UTF-8-Validität prüfen, nur für Bytes, die nicht Teil einer gültigen UTF-8-Sequenz sein können, Windows-1252-Fallback (`golang.org/x/text/encoding/charmap`)
- `pkg/mailparser/charset_repair_test.go` (neu) — 12 Testfälle
- `pkg/mailparser/parser.go``decodeMIMEHeader()`-Ergebnis, Single-Part-Body, Multipart-Text-/HTML-Buffer, Content-Type-Fehlerpfad laufen durch die Reparatur; `Attachment.Data` bewusst ausgenommen
- `internal/storage/subject_charset_backfill.go` (neu) — `ListNonASCIISubjects()`
- `cmd/archivmail/cmd_fix_subjects_candidates.go` (neu) — merged/dedupliziert Encoded-Word- und Charset-Repair-Kandidaten
- `cmd/archivmail/cmd_fix_subjects.go` — Filter erweitert auf `HasEncodedWord || NeedsCharsetRepair`
Stolperstein auf dem Server: Prefilter `octet_length(subject) <> length(subject)` liefert in einer `SQL_ASCII`-DB immer 0 Treffer, weil `length()` dort Bytes zählt. Ersetzt durch `subject ~ '[^[:ascii:]]'`.
Messung auf 192.168.1.132 (Dry-Run, read-only): 21.004 geprüfte Kandidaten (Encoded-Word- + Charset-Repair-Muster kombiniert), 54 zusätzliche Betreffs durch Charset-Repair betroffen (alle Tenant 3), 0 Fehlklassifikationen. Body-Verifikation über alle 52.961 Mails mit gepatchtem Parser: `parsedSubjectInvalidUTF8: 54 → 0`, `bodyInvalidUTF8: 219 → 0`.
Build, `go vet`, `go test ./pkg/mailparser/` auf 132 grün. `--apply` noch nicht ausgeführt.
## QA Test Results
_To be added by /qa_
## Deployment
_To be added by /deploy_
@@ -0,0 +1,66 @@
package storage
import (
"context"
"fmt"
)
// ListNonASCIISubjects returns emails whose subject contains at least one
// non-ASCII byte.
//
// Why a second lister next to ListRawEncodedSubjects: that one only matches the
// RFC 2047 encoded-word pattern (`=?charset?B|Q?...?=`) and therefore cannot
// find the second mojibake class at all — headers that were written with raw
// 8-bit Windows-1252/ISO-8859-1 bytes and no encoded-word syntax. Those rows
// contain no `=?...?=` marker; the only cheap SQL signal is "has bytes > 0x7F".
//
// The prefilter is `subject ~ '[^[:ascii:]]'`. Note that the obvious
// alternative `octet_length(subject) <> length(subject)` is WRONG here: in a
// SQL_ASCII database (which the production archive uses) length() counts bytes,
// so that condition is never true and the query silently returns nothing.
// The regex class works byte-wise in SQL_ASCII and char-wise in UTF8.
//
// The caller must narrow the result down with mailparser.NeedsCharsetRepair —
// the vast majority of non-ASCII subjects are perfectly valid UTF-8 and must
// not be touched.
//
// tenantID nil = all tenants. limit <= 0 = no limit.
func (s *Store) ListNonASCIISubjects(ctx context.Context, tenantID *int64, limit int) ([]SubjectRow, error) {
if s.db == nil {
return nil, fmt.Errorf("storage: list non-ascii subjects: no database configured")
}
query := `SELECT id, tenant_id, COALESCE(subject, '')
FROM emails
WHERE subject IS NOT NULL
AND subject ~ '[^[:ascii:]]'`
args := []interface{}{}
if tenantID != nil {
args = append(args, *tenantID)
query += fmt.Sprintf(" AND tenant_id = $%d", len(args))
}
query += " ORDER BY received_at ASC"
if limit > 0 {
args = append(args, limit)
query += fmt.Sprintf(" LIMIT $%d", len(args))
}
rows, err := s.db.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("storage: list non-ascii subjects: %w", err)
}
defer rows.Close()
var out []SubjectRow
for rows.Next() {
var r SubjectRow
if err := rows.Scan(&r.ID, &r.TenantID, &r.Subject); err != nil {
return nil, fmt.Errorf("storage: list non-ascii subjects: scan: %w", err)
}
out = append(out, r)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("storage: list non-ascii subjects: rows: %w", err)
}
return out, nil
}
+79
View File
@@ -0,0 +1,79 @@
package mailparser
import (
"bytes"
"unicode/utf8"
"golang.org/x/text/encoding/charmap"
)
// Charset repair for mails that carry 8-bit bytes without (or with a wrong)
// charset declaration.
//
// Two real-world cases in the archive that PROJ-84 did NOT cover:
//
// 1. Subject/From/filename headers with raw 8-bit bytes and no RFC 2047
// encoded-word at all (RFC 5322 forbids it, but many older newsletter and
// shop systems do it anyway). Go's mime.WordDecoder passes such a header
// through byte-for-byte, so a Windows-1252 "ü" (0xFC) ended up as an
// invalid UTF-8 byte in emails.subject and in Manticore.
//
// 2. text/* body parts without a charset parameter (or declared us-ascii)
// that nevertheless contain Latin-1/Windows-1252 bytes. decodeCharset()
// treats an empty/us-ascii charset as "already UTF-8" and returns the
// bytes unchanged — same broken result.
//
// RepairUTF8Bytes closes both gaps with the fallback strategy common in mail
// clients: assume UTF-8 first, and only for bytes that cannot be part of a
// valid UTF-8 sequence fall back to Windows-1252 (a superset of ISO-8859-1,
// which is what such senders almost always mean).
//
// Deliberate properties:
// - Input that is already valid UTF-8 is returned untouched (identity), so
// correctly encoded mails can never be mangled.
// - The repair is byte-wise, not buffer-wise: a header that mixes a properly
// decoded UTF-8 encoded-word with raw Latin-1 bytes keeps the UTF-8 part
// intact instead of being re-decoded as a whole.
// - It only ever touches the derived metadata/search text. The archived
// original (encrypted EML) is never rewritten — GoBD immutability.
func RepairUTF8Bytes(b []byte) []byte {
if utf8.Valid(b) {
return b
}
var out bytes.Buffer
out.Grow(len(b) + len(b)/4)
for i := 0; i < len(b); {
if b[i] < utf8.RuneSelf {
out.WriteByte(b[i])
i++
continue
}
// Prefer a genuine UTF-8 sequence when one is present.
if r, size := utf8.DecodeRune(b[i:]); r != utf8.RuneError || size > 1 {
out.Write(b[i : i+size])
i += size
continue
}
// Not valid UTF-8 at this position: interpret the single byte as
// Windows-1252. Undefined CP1252 bytes yield U+FFFD, which is still
// better than an invalid byte in the database/index.
out.WriteRune(charmap.Windows1252.DecodeByte(b[i]))
i++
}
return out.Bytes()
}
// RepairUTF8 is the string form of RepairUTF8Bytes.
func RepairUTF8(s string) string {
if utf8.ValidString(s) {
return s
}
return string(RepairUTF8Bytes([]byte(s)))
}
// NeedsCharsetRepair reports whether s contains bytes that are not valid UTF-8
// and would therefore be repaired by RepairUTF8. Metadata repair runs use it to
// pick candidate rows; it is intentionally cheap and side-effect free.
func NeedsCharsetRepair(s string) bool {
return !utf8.ValidString(s)
}
+78
View File
@@ -0,0 +1,78 @@
package mailparser
import (
"strings"
"testing"
)
func TestRepairUTF8(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"pure ascii untouched", "Passwort geaendert", "Passwort geaendert"},
{"valid utf8 untouched", "Passwort geändert ok", "Passwort geändert ok"},
{"windows1252 umlauts", "Passwort ge\xe4ndert", "Passwort geändert"},
{"windows1252 sharp s", "Mini-Fu\xdfball", "Mini-Fußball"},
{"cp1252 en dash 0x96", "Netbook f\xfcr 207 Euro \x96 jetzt", "Netbook für 207 Euro jetzt"},
{"cp1252 registered 0xae", "NVIDIA\xae Karten", "NVIDIA® Karten"},
{"mixed valid utf8 and latin1", "Gr\xfc\xdfe ünd", "Grüße ünd"},
{"empty", "", ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := RepairUTF8(tc.in); got != tc.want {
t.Errorf("RepairUTF8(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestNeedsCharsetRepair(t *testing.T) {
if NeedsCharsetRepair("Passwort geändert") {
t.Error("valid UTF-8 must not be flagged for repair")
}
if !NeedsCharsetRepair("Passwort ge\xe4ndert") {
t.Error("raw Latin-1 byte must be flagged for repair")
}
}
// A Subject header with raw 8-bit bytes and no RFC 2047 encoded-word must be
// decoded via the Windows-1252 fallback instead of ending up as invalid UTF-8.
func TestParseRawEightBitSubject(t *testing.T) {
raw := "From: a@example.com\r\n" +
"Subject: Passwort f\xfcr WoltLab ge\xe4ndert\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Gr\xfc\xdfe\r\n"
pm, err := Parse([]byte(raw))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if pm.Subject != "Passwort für WoltLab geändert" {
t.Errorf("Subject = %q", pm.Subject)
}
if !strings.Contains(pm.TextBody, "Grüße") {
t.Errorf("TextBody = %q", pm.TextBody)
}
}
// A correctly encoded UTF-8 mail must survive the repair unchanged.
func TestParseValidUTF8NotMangled(t *testing.T) {
raw := "From: a@example.com\r\n" +
"Subject: =?UTF-8?Q?Gr=C3=BC=C3=9Fe?=\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
"Schöne Grüße äöüß\r\n"
pm, err := Parse([]byte(raw))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if pm.Subject != "Grüße" {
t.Errorf("Subject = %q", pm.Subject)
}
if !strings.Contains(pm.TextBody, "Schöne Grüße äöüß") {
t.Errorf("TextBody = %q", pm.TextBody)
}
}
+23 -8
View File
@@ -197,7 +197,9 @@ func Parse(raw []byte) (pmOut *ParsedMail, errOut error) {
if err != nil {
// No content-type or parse error: treat as plain text
body, _ := io.ReadAll(msg.Body)
pm.TextBody = string(body)
// Mails without any Content-Type are typically old 8-bit Latin-1
// messages — repair them like a declared text/plain part.
pm.TextBody = string(RepairUTF8Bytes(body))
return pm, nil
}
@@ -211,11 +213,16 @@ func Parse(raw []byte) (pmOut *ParsedMail, errOut error) {
} else {
body, _ := io.ReadAll(msg.Body)
decoded := decodeBody(body, msg.Header.Get("Content-Transfer-Encoding"))
decoded = decodeCharset(decoded, params["charset"])
if strings.HasPrefix(mediaType, "text/") || mediaType == "" {
decoded = decodeCharset(decoded, params["charset"])
}
// The body always ends up as displayed/indexed text here, so the 8-bit
// fallback repair applies to every branch (undeclared charset, wrongly
// declared charset, or no usable Content-Type). No-op for valid UTF-8.
if strings.Contains(mediaType, "html") {
pm.HTMLBody = string(decoded)
pm.HTMLBody = string(RepairUTF8Bytes(decoded))
} else {
pm.TextBody = string(decoded)
pm.TextBody = string(RepairUTF8Bytes(decoded))
}
}
@@ -276,11 +283,13 @@ func parseMultipart(pm *ParsedMail, body io.Reader, boundary string, depth int)
continue
}
// Only the displayed/indexed body text gets the 8-bit fallback repair;
// attachment bytes above stay byte-exact on purpose.
switch {
case strings.Contains(mediaType, "text/plain"):
pm.textBuf.Write(decoded)
pm.textBuf.Write(RepairUTF8Bytes(decoded))
case strings.Contains(mediaType, "text/html"):
pm.htmlBuf.Write(decoded)
pm.htmlBuf.Write(RepairUTF8Bytes(decoded))
}
}
}
@@ -340,7 +349,13 @@ func decodeMIMEHeader(s string) string {
}
decoded, err := dec.DecodeHeader(s)
if err != nil {
return s
// Undecodable encoded-word: keep the raw value, but still repair raw
// 8-bit bytes so nothing invalid reaches the DB/index.
return RepairUTF8(s)
}
return decoded
// Headers without any encoded-word are returned byte-for-byte by
// WordDecoder. Senders that write raw Windows-1252/ISO-8859-1 bytes into
// the header would otherwise land as invalid UTF-8 in emails.subject.
// RepairUTF8 is a no-op for valid UTF-8.
return RepairUTF8(decoded)
}
+1
View File
@@ -88,4 +88,5 @@ export const features: Feature[] = [
{ id: "PROJ-82", name: "Print-Farbparität zwischen Hell- und Dark-Mode-Ausdrucken", status: "Planned", frontend: true, backend: false, lastUpdated: "2026-08-06", version: "1.0" },
{ id: "PROJ-83", name: "Audit-Logging für Anhang-Abrufe (GoBD/DSGVO-Nachbesserung)", status: "Planned", frontend: false, backend: true, lastUpdated: "2026-08-06", version: "1.0" },
{ id: "PROJ-84", name: "Fix MIME-Header-Charset-Dekodierung + Backfill für Bestandsmails", status: "In Review", frontend: false, backend: true, lastUpdated: "2026-08-06", version: "1.0" },
{ id: "PROJ-85", name: "Fix undeklarierte 8-Bit-Zeichen in Header/Body ohne Encoded-Word", status: "In Review", frontend: false, backend: true, lastUpdated: "2026-08-06", version: "1.0" },
];