bmc_hub/fremtidige planer/indgaaende-mail-ny-arkitektur.md

261 lines
32 KiB
Markdown
Raw Permalink Normal View History

# Indgående mail: fælles indbakke, sikker sagskobling og varig behandling
**Status: Delvist under udvikling lokalt. Den samlede arkitektur er ikke implementeret.**
**Dato: 4. oktober 2026.**
## Lokal delimplementering 29. oktober 2026
Autoriseret første pakke: enklere højrepanel og bedre afsendergenkendelse. Indgående svar på kendte sager/tråde skal behandles automatisk uden manuel afsendergodkendelse.
- Højrepanelet samler Afsender og Sag; vurdering og tekniske handlinger er foldet sammen. De store, farvede handlingsknapper bevares øverst over mailen.
- Nyt read-only sender-identity-endpoint viser alle eksakte kontakt-, mail- og domænematches til aktive kunder og leverandører. Flere mulige tilknytninger præsenteres til valg.
- Operatøren kan søge eksisterende kunder/leverandører. Ny tilknytning fra afsenderpanelet ændrer kun den valgte mail. Ældre API-klienter beholder eksisterende automatisk domænelagring; det nye panel bruger `remember_domain: false`.
- Automatisk routing af kendte svar bevares. En gemt sags-/trådrelation kan nu også overvinde klassifikation som spam/nyhedsbrev. Ukendte nyhedsbrevstråde får ikke denne undtagelse. Globale deaktiveringsindstillinger respekteres fortsat.
- Testet lokalt med mockede databaser/providers: 27 Python-tests og 12 JavaScript-tests. JavaScript-syntaks og template-parsing kontrolleres separat. Ingen migration, ekstern mailafsendelse eller produktionsudrulning.
- To direkte valg er tilføjet under de fire eksisterende store knapper: »Knyt til sag« åbner og fokuserer eksisterende sagsøgning; »Leverandørsag« vises for kendt leverandør og åbner et lille panel med titel fra mailen og eksplicit kundevalg. Eksisterende create-sag genbruges med leverandørsagstype og kontaktreference; der oprettes ikke domæneregel for leverandøren.
- Safari-kontrol af nye knapper: åbning, fokus, kundesøgning, valg, aktivering af Opret og Annuller er verificeret. Ingen sag oprettet og ingen mail flyttet som del af kontrollen.
- Et entydigt eksakt kontaktmatch tilknyttes nu automatisk i indbakken, uden at gemme domæneregel eller overskrive eksisterende tilknytning. Flere matches og domænematches alene kræver fortsat valg. Almindelige Globalconnect-mails får ikke længere teksten »Internetfaktura genkendt«.
- Leverandørkontakter matches via `vendor_contacts` og `contacts`; læsende kontrol i lokal database bekræfter et entydigt kontaktmatch til Globalconnect for den rapporterede afsender.
- Safari-genindlæsning af den berørte mail er udført; efterfølgende læsende databasekontrol bekræfter automatisk tilknytning til leverandør 6. Den afsluttende visuelle kontrol blev afbrudt af timeout i browserstyringen. Den samlede pipeline, konflikthåndtering, varig kø og historisk oprydning er fortsat planlagt.
### Ordrebekræftelse og levering: lokal udvidelse
Under »Ordre og levering« findes nu to valg: Ordrebekræftelse og Levering / tracking. Begge åbner samme bestillingsvælger med søgning på vare, ekstern reference, kunde og sag. Der vises højst 15 af de senest opdaterede indkøbslinjer; annullerede indkøb og slettede sager udelades. Seneste liste er ikke et automatisk matchforslag.
Eksplicit bekræftelse gemmer sagstilknytning, valgt mailtype, eventuelt manuelt angivet trackingnummer og bestillingsreference i mailens eksisterende aktivitetslog i én transaktion. Gentaget identisk handling er idempotent; en anden eksisterende sag eller bestillingsbeslutning giver konflikt. Ingen indkøbsstatus, økonomisk eksport, ekstern notifikation eller workflow udføres af denne handling. Ingen migration kræves.
Valideret: 22 fokuserede Python-tests og 13 JavaScript-tests. Lokal API genstartet. Bestillingslisten er verificeret læsende mod containerens database (9 valgmuligheder). Safari-kontrol verificerer menu, begge paneltyper, trackingfelt, bestillingsvalg, aktivering af gemmeknap og annullering. Ingen rigtig mail blev knyttet til en bestilling som test; skriveforløbet er dækket med mockede transaktioner, inklusive fejl/rollback og konflikter.
### Ensartet betjening
Alle mailhandlinger bruger nu samme store knapkomponent i ét grid; ordre- og leveringsvalg er direkte synlige. Afsender- og sagsøgning i højrepanelet er altid synlig. Alle oplysnings- og handlingssektioner i højrepanelet er permanent synlige med almindelige overskrifter. Ingen fold-ud-pile eller Vis/Skjul-knapper. Handlingsformularer åbnes fortsat ved deres respektive handlingsknap. »Kun arkivér« kan altid bruges uden kendt kunde, leverandør eller kontakt; de øvrige flows bevarer deres identitetskrav. Layoutet er kontrolleret visuelt i den indloggede Safari-session. 13 JavaScript-tests består.
### Opret virksomhed og kontakt fra mail: lokal udvidelse
Afsenderpanelet har nu ét direkte valg: »Opret kunde / leverandør + kontakt«. Formularen foreslår navn, mail, direkte telefon, mobil, stilling, afdeling, virksomhedsnavn, CVR, adresse, postnummer, by og hjemmeside fra afsenderheaderen og den nyeste mails signatur. Citerede ældre mails afskæres før udtræk. Et valgfrit AI-valg kan supplere forslagene; alle felter er redigerbare og skal bekræftes før skrivning.
Eksisterende virksomheder foreslås ud fra eksakt navn, CVR, mail og sikkert domænematch. Eksisterende kontakter foreslås ved eksakt mailadresse. Valgte eksisterende poster overskrives ikke. Ny virksomhed, ny kontakt, relation og mailtilknytning gemmes i én transaktion med dubletkontrol, lås, audit og gentagelsessikker payload. Der oprettes ingen domæneregel automatisk.
Valideret lokalt: 40 relevante Python-tests og 13 JavaScript-tests består, inklusive gamle mailflows. Safari-kontrol på Nannas mail viser Leverandør, Globalconnect og den eksisterende kontakt Nanna Søndergaard Skov valgt automatisk. Gem blev ikke aktiveret under kontrollen, og der blev ikke oprettet testdata.
### Kontrolleret lokal aktivering af automatisk mailbehandling
Den 29. oktober 2026 blev det planlagte mailjob aktiveret i den lokale Docker-konfiguration med fem minutters interval. `EMAIL_REQUIRE_MANUAL_APPROVAL=true` bevares: kun verificerede svar med en gemt sag-/trådrelation kan omgå den manuelle gate; nye og tvetydige mails kan ikke automatisk oprette eller vælge sag. Autokør-endpointet er samtidig aktiveret.
Første planlagte kørsel blev observeret i den kørende proces: 50 mails hentet og klassificeret på 3,7 sekunder uden fejl. 47 blev parkeret som `awaiting_user_action`; tre forblev `new`, fordi en almindelig reply-header tidligere blev behandlet som et routinghint uden et databasebevist forældrematch. Gaten er efterfølgende strammet: en reply-header alene giver ikke længere automatisk behandling. Kun gemt sag-/trådmatch eller eksplicit BMC-sagsmarkør kan omgå manuel kontrol. Ingen af de 50 blev markeret `auto_processed` eller knyttet til sag, så første levende succes afventer en ny mail med en kendt relation. Denne afgrænsning og automatisk kendt reply-routing er dækket af 41 relevante Python-tests.
## Beslutning og afgrænsning
Retænk modulet som en arbejdsindbakke oven på en pålidelig mailpipeline. Bevar eksisterende mail-ID'er, Graph/IMAP/SMTP, sagsmail, reply/forward, import, regler og specialflows. Genbrug PostgreSQL, eksisterende scheduler, services og sagskomponenter; første version kræver hverken en ny broker eller en separat mailplatform.
Denne plan bygger på fem særskilte kodegennemgange: nuværende flow, sagskobling, UX, robusthed og datamodel/API. Fundene beskriver kodeverificerede risici, ikke dokumenterede produktionshændelser. Ingen mail er hentet eller sendt som del af gennemgangen. Ingen runtimekode eller database ændres af planen.
## Nuværende flow og konkrete problemer
Scheduler → Graph/IMAP → gem mail/filer → transskription → klassifikation → godkendelsesgate → systemworkflows og konfigurerede workflows → legacyregler → sagsrelationer og lokal processed-status.
| Fund | Evidens | Ændring i forslaget |
|---|---|---|
| Kun seneste N mails hentes; varig beskedcursor mangler | `app/services/email_service.py:411-412,487-507` | Paginering, checkpoint og genoptagelig backlog |
| Providerfejl returnerer tom liste og kan ligne tom indbakke | `email_service.py:448-453,499-503`; `email_processor_service.py:81-83` | Resultat med eksplicit syncfejl og sidst lykkede synkronisering |
| Gemte mails med efterfølgende fejl kommer ikke automatisk tilbage i behandling | `email_processor_service.py:85-114` | Varig jobkø uafhængig af import |
| En vellykket workflow kan skjule en anden fejlet workflow | `email_processor_service.py:207-223` | Resultater pr. obligatorisk handling; delvis succes er synlig |
| `folder` bruges både til kildemappe og lokal arbejdsstatus | `email_service.py:352-373`; `email_processor_service.py:217` | Separate providerplaceringer og triagestatus |
| Første mail autoåbnes, og detail-GET kan markere den fælles læst | `app/emails/frontend/emails_v2.html:848-859`; `app/emails/backend/router.py:1030-1073` | Nye GET-endpoints er uden sideeffekter; eksplicit kvittering |
| Aktive mails filtreres blandt seneste 150; tællere gælder kun den hentede liste | `emails_v2.html:748-758,814-840` | Backendfiltre, pagination og globale køtal |
| Modstridende tråd- og sagsreferencer afgøres med første/seneste fund | `app/services/email_workflow_service.py:456-514,1170-1254` | Indsaml alle kandidater og parkér konflikter |
| Sagskobling og journal udføres i flere separate commits | `email_workflow_service.py:616-652`; `app/emails/backend/router.py:1265-1420` | Atomiske commands med lås og idempotens |
| Join-tabel og `linked_case_id` kan være uenige | `app/modules/sag/backend/router.py:5864-5877,5997-6002`; `app/emails/backend/router.py:1556-1604` | Én autoritativ relation, gamle felter som kompatibilitetsprojektion |
| Standardvisningen mangler dele af V1's oprettelsesvalg og har faste sagstyper | `emails_v2.html:1320-1335,1801-1808`; `emails.html:2690-2706` | Fælles sagsopretter med systemets sagstyper |
| Én gemt vedhæftning kan skjule, at andre mangler | `email_processor_service.py:142-151`; `email_service.py:1388-1430` | Manifest og status pr. vedhæftning |
| Filer mangler relevant dedupe-constraint, og originalnavn indgår i storage-sti | `migrations/013_email_system.sql:99-115`; `email_service.py:1404-1425` | Unik MIME-partidentitet og genereret sikker storagekey |
| Download bruger ikke lagrede DB-bytes, hvis diskfil mangler | `app/emails/backend/router.py:1160-1190` | Samme downloadservice med verificeret disk-/DB-fallback |
| Graph-fejl udløser SMTP-fallback uden særskilt ukendt delivery | `email_service.py:1996` | Outbox og afklaring før potentiel gensendelse |
## Den samlede arkitektur
```mermaid
flowchart TD
P[Graph / IMAP / EML / MSG] --> S[MailboxSyncService]
S --> I[MailIngestService: original, delivery, MIME, filer]
I --> Q[PostgreSQL-jobkø]
Q --> R[CaseMailRoutingService: kandidater og beviser]
Q --> A[Analyse og eksisterende specialworkflows]
R --> T[Triage: ansvar, handling, vurdering]
T --> C[CaseMailCommandService]
C --> J[Sagsrelation og journal i én transaktion]
C --> O[Outbox for eksterne handlinger]
J --> U[Fælles indbakke og sagens mailvisning]
T --> U
```
### Ansvarsfordeling
1. **MailboxSyncService:** Provideradaptere, fuld paginering, synkroniseringscursor, rate limits og synlig forbindelsesstatus. Ingen AI eller sagsoprettelse. IMAP forbliver read-only med PEEK. Graph skal bruge stabil beskedidentitet; ændret mappe er en delivery-opdatering, ikke en ny mail.
2. **MailIngestService:** Gem original og normaliseret mail, kildeidentitet, headers og vedhæftningsmanifest. Opret behandlingsjobs i samme DB-transaktion. Checkpoint flyttes først efter varig lagring. En ugyldig mail kan gemmes som fejlet import med originalen, så den ikke stopper resten af postkassen.
3. **MailJobRunner:** PostgreSQL-kø med `FOR UPDATE SKIP LOCKED`, lease, forsøg, næste køretid og backoff. Ingen DB-lås under netværkskald. Udløbne leases kan overtages; effektens idempotens beskytter mod gentagelse. Manuel genbehandling genoptager manglende arbejde.
4. **CaseMailRoutingService:** Ren kandidatberegning og begrundet beslutning; samme regler for indbakke, jobs og sagsvisning. Ingen writes under preview.
5. **MailTriageService:** Arbejdsbehov, ansvarlig/gruppe, prioritet, vurderingsgrund, læsning og fælles kvittering. Behandling og visning ændrer ikke automatisk sagens status.
6. **CaseMailCommandService:** Opret, knyt, flyt, tilføj relation og frakobl gennem fælles transaktionskode. Valider payload og adgang før writes. Lås mail/arbejdsenhed; sag, relation, journalhændelse, audit og videre jobs gemmes samlet.
7. **WorkflowRunner:** Genbrug eksisterende regler og actions bag adaptere. Gem en versioneret eksekveringsplan og status pr. handling. Retry gentager ikke gennemførte handlinger. Nødvendige og valgfrie handlinger er eksplicitte.
8. **MailOutboxService:** Varige sende-/notifikationsintentioner med stabile ID'er. Skeln mellem afvist, accepteret og ukendt afsendelsesresultat. Ingen automatisk SMTP-fallback efter et muligvis accepteret Graph-kald.
Graph bruger fuld bootstrap-paginering og derefter delta/checkpoint, hvor providerens kontrakt understøtter det; udløbet cursor giver afstemt resync, ikke tavs nulstilling. IMAP bruger UIDVALIDITY og UID pr. mappe. Skift af postkasse/provider opretter en ny kildeidentitet; credentialsrotation alene gør ikke. Scheduler kan fortsat udløse arbejdet, men varige leases er autoriteten ved flere processer.
## Kobling af mails til sager
### Kandidater og prioritering
Alle stærke signaler indsamles, før noget kobles. Prioriteringen hjælper med entydige tilfælde; den må ikke skjule en konflikt.
1. Tidligere eksplicit manuel beslutning eller trådsplit med versionshistorik.
2. Gyldigt, serverregistreret scanner-token eller nyt tilfældigt reply-token knyttet til sag og tråd. Token i citeret/videresendt tekst er ikke alene autorisation.
3. Eksakt kendt parent fra `In-Reply-To`; dernæst nærmeste kendte `References`, nyeste først.
4. Mailbox-afgrænset provider-conversation med entydig kobling.
5. Eksisterende BMCid/SAG-tag med deltager-, kunde- og konfliktkontrol; legacy Message-ID-markør som kompatibilitetsfallback.
6. Eksakt kontakt, domæne, emnelighed og AI giver forslag til eksisterende sag. Et kundedomæne alene identificerer ikke en sag.
Modstridende sag/kunde, flere stærke kandidater eller slettet sag giver **Kræver vurdering**. Systemet opretter ikke en ekstra sag som fallback efter en konflikt. Nye leverandører og CC-deltagere kan legitimt svare; ukendt afsender betyder derfor ikke automatisk afvisning. Kendt modstridende kunde kræver stillingtagen.
Sikkert reply på lukket sag vises som **Ny mail på lukket sag**, uden automatisk genåbning. Brugeren kan beholde relationen, genåbne eller oprette en relateret sag. Eventuel automatisk genåbning kræver en særskilt, synlig indstilling.
Forward starter normalt en ny tråd med reference til kildetråden. Citerede headers/BMCid giver forslag. Eksisterende videresendelse af seneste mail eller hele tråden bevares. Et bestemt antal mailtråde må aldrig udledes af, at de ligger på samme sag.
Automatisk oprettelse for kendte kunder bevares som særskilt indstilling, fortsat deaktiveret som standard. Entydig kundekobling, policy, spam-/loopkontrol og ingen matchkonflikt er forudsætninger. Scannerens tidsheuristik nedgraderes til forslag; egentlige validerede dokumenttokens bevares til automatisk kobling.
### Relationer og journal
En sag kan have mange tråde. En mail kan have én primær sagstilknytning og flere supplerende relationer. Konkursflowets eksakte CVR-match til flere kundesager bevares; hver relation har eget journal-event og adgangskontrol. Der må ikke følge deltagerinformation eller mails fra én kundesag automatisk med til en anden.
Indgående mail giver én identificerbar journalhændelse pr. mail og sag med ny tekst, link til original/tråd og filstatus. Genbehandling giver ikke flere kommentarer. Bevar eksisterende kommentarer; første version kan projicere nye mail-events gennem sagens eksisterende kommentarrenderer med en unik `source_email_id`/eventnøgle.
**Flyt primær tilknytning**, **tilføj supplerende relation**, **frakobl** og **fortryd** er forskellige commands. Begrundelse og audit gemmes. Et manuelt afvist match må ikke genopstå ved retry. Ved flytning beholdes en historisk journalreference med rettelsesmarkering; originalmailen slettes ikke.
## UX: arbejdsindbakken
Desktop har arbejdslisten til venstre, mail/tråd i midten og kunde/sag/handlinger til højre. Mobil bruger liste, tråd og handlingspanel som separate visninger.
Backendberegnede køer: **Kræver handling**, **Ikke tildelt**, **Mine**, **Min gruppe**, **Nye på sager**, **Afventer**, **Fejl** og **Arkiv**. Ulæst er et separat filter. Rækker viser kunde/afsender, emne, seneste tekst, antal ulæste, alder, ansvarlig/gruppe, prioritet og primær sag. Køer omfatter også ældre mails; filtre og tællere gælder hele det autoriserede datasæt.
Seneste nye tekst vises først, med foldbar signatur/citering og adgang til original og headers. Ingen automatisk kvittering ved indlæsning. Vis dokumenter med navn, størrelse og hentnings-/sikkerhedsstatus.
Primære handlinger: **Knyt til sag**, **Opret sag**, **Leverandørfaktura**, **Bogholderi**, **Arkivér**. Foreslåede sager viser matchbegrundelse, kunde, status og seneste aktivitet. Kundevalg opretter ikke automatisk en permanent domæneregel; “Husk dette domæne” er særskilt.
Sagsopretteren genbruger titel, ny tekst, kunde, kontakt, systemets konfigurerede sagstyper, prioritet, ansvarlig, gruppe, startdato og deadline. Historiske sagstyper bevares på eksisterende sager. Før oprettelse vises hvilke mails/filer, der følger med.
Composer skelner tydeligt mellem **Svar** og **Intern kommentar**. Svar viser To/Cc og reply-all før afsendelse. Plusmenu til tildeling med begrundelse, tidsregistrering, løsning, vedhæft og afventer genbruger sagens funktioner. Intern kommentar kan ikke implicit blive kundemail. Tid og løsning kræver relevant sag og eksisterende valideringsregler.
Tråde uden sag kan have køansvarlig. Sagstilknyttet arbejdsbehov arver som standard sagens ansvar; en lokal køoverdragelse ændrer ikke sagens ansvar uden en særskilt handling. AI-hast, sentiment og kunderisiko er adskilte rådgivende signaler. Ingen automatisk ændring af prioritet, SLA eller ansvar i første version, jf. `kundestemning-ai.md`.
### Adskilte tilstande
| Dimension | Eksempler | Betydning |
|---|---|---|
| Personligt set | bruger + seen_at | Hvad den enkelte har åbnet |
| Fælles kvittering | bruger + acknowledged_at | Ansvarlig/gruppen har taget imod beskeden |
| Arbejde | kræver handling / afventer / afsluttet / arkiveret | Henvendelsens behandling, ikke sagens lukning |
| Teknisk behandling | queued / running / retry / failed / completed | Pipeline og nødvendige handlinger |
| Kobling | ukoblet / foreslået / tilknyttet / konflikt | Begrundet relation |
| Prioritet | eksisterende kontrollerede prioriteter | Menneskeligt triagevalg, adskilt fra alder og AI |
Ny indgående reply skaber nyt arbejdsbehov og ulæstindikator, selv om tidligere mails er afsluttet. Både indbakke og V4/V5 læser samme tilstand. En reply gør ikke automatisk hele sagen åben eller løst.
## Datamodel: additive ændringer
Undersøg faktisk installationsschema før migration. Bevar eksisterende `email_messages`, `email_attachments`, `email_analysis`, `email_activity_log`, workflows og `sag_emails` med deres ID'er. Historiske migrationer genkøres ikke.
| Tabel/udvidelse | Rolle og centrale constraints |
|---|---|
| `mail_sources`, `mail_sync_states` | Stabil kildeidentitet, provider, postkasse; cursor pr. kilde/mappe, sidste succes/fejl og lease. Referér eksisterende krypteret konfiguration, kopiér ikke secrets. |
| `email_deliveries` | `email_id`, kilde, provider-ID, folder, UIDVALIDITY/UID eller Graph immutable ID. Unik stabil provideridentitet pr. kilde; samme mail kan findes flere steder. |
| `email_messages` ekstra felter | Faktisk RFC Message-ID, raw hash/storage-reference, normaliserede headerdata, retning, parseversion og ny tekst. Gamle `message_id` og globale UNIQUE bevares som legacyidentitet. Nye kollisioner får separat intern legacyværdi, mens faktisk RFC-ID lagres korrekt. |
| `email_threads`, medlemskab og provideraliases | Stabil intern tråd-ID, mailbox-afgrænsede aliases og parentrelationer. En besked har én aktuel normal tråd; flyt/split auditeres. Sagsrelation bestemmer ikke tråd-ID. |
| `mail_work_items` | Arbejde pr. tråd og primær sag/ukoblet kø; status, ansvarlig/gruppe, prioritet, seneste indgående besked og versionsnummer. Særskilte kundearbejder ved legitime fler-sagsflows. |
| `email_user_state` og kvittering | Unik `(email_id,user_id)` personlig set-state. Fælles kvittering bevarer ansvarlig-/gruppereglen og hvem/hvornår. |
| `sag_emails` metadata | Primær/supplerende rolle, aktiv/inaktiv, kilde, beslutnings-ID og audit. Højst én aktiv primær relation pr. mail; flere supplerende tilladt. `linked_case_id` er projektion af primær relation. |
| `email_routing_decisions` | Kandidater, beviser, algoritmeversion, konflikt, valgt handling, aktør og fravalg. Manuel beslutning kan kun erstattes eksplicit. |
| `email_notes` og kopieringsreference | Interne triagenoter før sagoprettelse; unik source-note-reference ved senere journaloverførsel, så noter ikke kopieres igen. |
| `mail_jobs`, action-runs | Unik job/effect-nøgle, payloadversion, lease, forsøg, next_attempt_at, fejl og permanent parkering. Unik workflowhandling pr. mail/planversion/step/mål. |
| `mail_outbox` | Varig intention, stabil sendnøgle, modtagere, case/thread, attempts, provider-ID og accepted/unknown/rejected. Simuleret afsendelse er ikke faktisk sent. |
| Vedhæftningsmanifest + casefilkilde | MIME part-ID, originalnavn, sikker storagekey, SHA-256, forventet/modtaget størrelse, CID/disposition, status/fejl. Unik part pr. mail; unik casefilreference pr. sag/source-attachment. |
| Mail-journalhændelse | Unik `(email_id,sag_id,event_kind)`; kommentar/event-ID og audit. Undgå dedupe på bodytekst. |
Dedupe sker først på sikker deliveryidentitet. RFC Message-ID normaliseres separat fra legacyfeltet og sammenholdes med indhold/proveniens. Samme RFC-ID med forskelligt indhold bliver en synlig kollision, ikke overwrite eller tavs udeladelse. Manglende ID får syntetisk intern ID baseret på kilde og varig deliveryidentitet. Indholdshash alene må ikke samle forskellige leverancer fra kunder eller fjerne en legitim ny henvendelse.
Indekser: jobstatus/next_attempt_at, kilde/provider-ID, normaliseret RFC-ID, parentreferences, thread/seneste aktivitet, aktive sagrelationer, triagestatus/ansvarlig/gruppe/prioritet og ulæststate. Brug `TIMESTAMPTZ` for nye tidsfelter. Normaliserede identiteter og faste statuser er kolonner; analyse/evidens kan være JSONB.
## API og kompatibilitet
Nye routes under `/api/v1/mail/...`; eksisterende `/api/v1/emails`, sagsmailroutes og V1/V2 bevares via adapters. Legacy detail-GET's læsebivirkning bevares under overgang; ny UI anvender kun rene GET og eksplicitte commands. Fjernelse af bivirkningen fra gammel API kræver en senere eksplicit beslutning.
| Endpointforslag | Kontrakt |
|---|---|
| `GET /mail/work-items` | Serverfiltre, stabil cursor med ID-tiebreaker, pagestørrelse og næste cursor |
| `GET /mail/queue-counts` | Samme autoriserede kødefinitioner som liste; count-scope beskrives |
| `GET /mail/threads/{id}` | Tråd, deltagerliste, paginerede beskeder og relationsstatus; ingen writes |
| `GET /mail/messages/{id}` | Original/ny tekst, headers, filmanifest og audit; ingen writes |
| `POST /mail/messages/{id}/seen` | Personlig set-state; ændrer ikke fælles arbejdsstatus |
| `POST /mail/messages/{id}/acknowledge` | Fælles kvittering med eksisterende ansvarlig-/gruppetilladelse |
| `POST /mail/messages/{id}/routing-preview` | Kandidater og begrundelser uden writes |
| `POST /mail/messages/{id}/case-link` og `/case-create` | Atomisk handling; rolle/sag/kunde/begrundelse, expected_version og Idempotency-Key |
| `POST /mail/messages/{id}/case-unlink` | Eksplicit frakobling og fravalg, ikke sletning af mail |
| `POST /mail/threads/{id}/split` | Valgte beskeder, måltråd og begrundelse; ingen implicit sagsflytning |
| `PATCH /mail/work-items/{id}` | Tildeling, prioritet, afventer/afslut med versionskontrol |
| `POST /mail/work-items/{id}/comment` | Intern kommentar; sag kræves for sagsjournal, ellers intern triagenote |
| `POST /mail/threads/{id}/reply` | Validerede modtagere, kildebesked og outbox-ID; 202 betyder queued, ikke sent |
| `POST /mail/jobs/{id}/retry` | Genoptag manglende trin, aldrig automatisk genafsend ukendt delivery |
| `GET /mail/sources/{id}/sync-status` | Sidste succes, lag, backlog og maskeret fejl |
Idempotency-Key gemmes med aktør, operation og payloadhash; samme key/payload returnerer samme resultat, ændret payload giver 409. Versionskonflikt giver 409 med opdateret ressource. Læsning, tilknytning, tildeling, arkivering, filer og afsendelse bruger fælles ressourcetilladelser; autentifikation alene giver ikke adgang til alle kundemails. Fejl returnerer stabil kode, dansk handlingsbesked og correlation-ID uden secrets.
## Robusthed og mailpolitik
- **Dubletter/samtidighed:** delivery-upsert og constraints; import, job og klik kan gentages uden nye sager, kommentarer, tidslinjer eller filer. Soft-deleted mails genimporteres ikke automatisk som nye.
- **Replies/threading:** bevar fulde rå headers; nærmeste kendte parent vægtes. Ukendt parent kan afklares, når en ældre besked importeres. Det må ikke overskrive en manuel beslutning.
- **Forwards/MIME:** top-level afsender/headers adskilles fra vedhæftet `message/rfc822` og citeret historik. Charset og multipart/alternative behandles korrekt; tekstfiler må ikke blive body. Inline CID-billeder har separat rolle.
- **Filer:** grænser før fuld indlæsning for mailstørrelse, antal dele, MIME-dybde og samlet størrelse; begrænset udpakning/OCR/transskription. Sikre genererede storagekeys uden klientfilnavne. Bevar originale navne som metadata. Kontroller faktisk indhold, MIME og checksum; karantæne ved uafklaret sikkerhed. En manglende del er synlig. Invoice-/dokumentflows må ikke afsluttes med ufuldstændige nødvendige filer. Download genbruger DB-bytes, når diskfil mangler, og giver en tydelig fejl, hvis begge kilder mangler.
- **Loops/autosvar:** registrér Auto-Submitted, Precedence, List-ID, delivery-status, interne senderidentiteter og egne correlationheaders. Gem beskederne, men undlad automatisk svar/ny sag på egne notifikationer, bounces og autosvar. Manuel behandling forbliver mulig. Ratebegræns autosvar pr. kilde/tråd/modtager.
- **Spam:** behold klassifikation/regler og separat karantæne. Spam/nyhedsbrev er reversible dispositioner; intet slettes alene på AI-resultat. Modstridende stærkt sagsmatch vises til vurdering, uden auto-response.
- **Fejl:** providerfejl, parsefejl, analysefejl, sagskonflikt og ekstern delivery har forskellige fejlkoder. AI-fejl stopper ikke import eller deterministic routing. Retry med backoff/Retry-After og begrænsede forsøg; derefter fejl-kø med sikker manuel genoptagelse.
- **Eksterne effekter:** lokal transaktion giver ikke exactly-once SMTP. Timeout efter send giver `unknown`, som afklares via provider eller manuelt. Fallback/retry må kun sende igen, når tidligere forsøg sikkert ikke er accepteret. Bevar eksisterende test/dry-run-policy og registrér simulation særskilt.
- **Indhold:** genbrug HTML-sanitering og blokér aktive elementer/fjern tracking som standard. Rå original kræver autoriseret download. AI-input behandles som maildata, ikke som instruktioner til værktøjer/actions.
## Eksisterende funktioner, der skal bevares
Graph og IMAP/SMTP; krypteret konfiguration og miljøfallback; manuelt force-run; .eml/.msg; read-only hentning; klassifikation og manuel godkendelse; workflowpreview, regler, bulk, arkiv, genbehandling og aktivitetslog; kunde-/leverandørvalg og domæneregler; reply/reply-all, send og videresend seneste/hel tråd; flere tråde pr. sag; scannertokens, BMCid og historiske routingmarkører; vedhæftningsrecovery og sagsfiler; lydtransskription; leverandørfaktura/ALSO/tracking/tid/konkursflows. Ældre tticket-actions bevares bag adapter og flyttes ikke stiltiende til SAG.
Begrundede ændringer: ingen automatisk læsekvittering i ny UI; ingen tavs vinder ved matchkonflikt; scanner-tid alene er forslag; ingen processed-status ved nødvendig delvis fejl; ingen blind gensendelse efter uklar accept. Alle ændringer leveres bag flags med dokumenteret gammel/ny adfærd.
## Migration og levering
1. **Baseline og kontrakttests:** kortlæg installeret schema, aktive workflows, eksisterende API-responses og permissions. Fixtures skal omfatte support, økonomi, konkurs, scanner, forward og lyd. Ingen eksterne testwrites.
2. **Additiv lagring og sync:** nye tabeller/felter, varig importcursor, tydelig syncstatus og manifest. Backfill i små genoptagelige batches. Bevar gamle tabeller og ID'er.
3. **Jobkø og atomiske commands:** fælles service bag eksisterende routes; versionerede actionruns og idempotens. Aktiver én write-path pr. flow, så gammelt job og nyt job ikke udfører samme handling.
4. **Routing i shadow mode:** beregn og sammenlign beslutninger uden sagswrites, journal, notifikation eller filkopiering. Historiske modstridende relationer markeres til vurdering; de samles eller slettes ikke automatisk.
5. **Ny indbakke:** ny route/flag, fælles komponenter med V4/V5, eksplicit set/kvittering, serverkøer og sagsreview. V1/V2 findes stadig.
6. **Kontrolleret aktivering:** først sikre replies/entydige matches, derefter specialflows ét ad gangen. Overvåg backlog, cursorlag, retries, konflikter, delvise filer og ukendt afsendelse.
Historisk `linked_case_id` bliver primær, hvis den er gyldig og konsistent; øvrige relationer bevares som `legacy_unclassified`, indtil rollen kan dokumenteres. Konflikter får review. Global `is_read` kan blive historisk fælles kvittering med ukendt aktør; opfind ikke personlige læseevents. `Processed` bliver ikke automatisk bevis for besvaret mail eller lukket sag. Kildemappe kan være ukendt, indtil providerafstemning; gæt den ikke ud fra processed-status. Tilføj håndhævede constraints efter backfill-validering, ikke ved at slette konflikter.
Rollback slukker nye UI/routingflags og standser nye workers, før gamle jobs genaktiveres. Allerede gennemførte effekter bevares og skal være dedupe-synlige for gamle adapters. Ingen down-migration sletter nye mails, relationer eller audit. Produktionsudrulning er en separat opgave.
## Kontrol og accept
| Område | Nødvendige kontroller |
|---|---|
| Import | Mere end fetchlimit; alle sider; crash før/efter checkpoint; IMAP UID-reset; Graph mappeflyt; timeout/429/tokenfejl; ændring af kildekonfiguration |
| Identitet | Samme delivery to gange; RFC-ID med/uden brackets; manglende ID; samme RFC-ID forskelligt indhold; identiske mails på flere postkasser; soft-delete |
| Sagskobling | Konflikt token/tag/parent; delt domæne; ukendt CC/leverandør; flere tråde pr. sag; lukket/slettet sag; forward med citeret BMCid; legacyrouting; unlink + retry |
| Atomaritet | Samtidigt klik/job; crash mellem sag/link/journal; gentagen payload; ændret payload samme key; uniqueconstraints; konkurs til flere kunder |
| Filer | Delvist download; null/zero bytes; samme navn forskelligt indhold; path traversal; inline CID; nested EML; store/deep MIME; recovery og kopi uden dubletter |
| Workflows | En succes + en nødvendig fejl; genoptag kun manglende trin; ændret workflowversion; AI utilgængelig; faktura/ALSO/tracking/tid/konkurs/scanner/lyd bevares |
| Send/loops | Accepteret send efterfulgt af timeout; ingen blind fallback; bounce/autosvar/egen mail; rategrænse; dry-run sender intet |
| UX/adgang | Indlæsning ændrer intet læst; gammel aktiv mail efter 150 nyere; globale køtal; system-sagstyper; ansvar/hastegrad før opret; personlig læsning vs. fælles kvittering; ingen krydskunde-læk |
| Kompatibilitet | Gamle responses/paths/statusprojektioner, V1/V2 og V4/V5; eksisterende filer, kommentarer, relationer og API-consumers |
Browserkontrol på lokalt testdatasæt og mobilbredde; integrationskontrol med fake providers og rollback-transaktioner. Dokumentér separat hvad der er kode-/testverificeret, browserverificeret og endnu ikke verificeret i drift.
## Første implementeringspakke efter accept
Afgræns første pakke til providerstatus/paginering, deliveryidentitet, varig jobkø, vedhæftningsmanifest og atomisk opret/kobl-command med kontrakttests. Derefter entydig routing og review. Den nye UX kommer, når tilstande og kommandoer er stabile. Implementér ikke hele omlægningen i én ændring.