Der Postfachabgleich hat gefundene Personen bisher direkt in den Kontaktbestand geschrieben. Damit wächst eine gepflegte Liste ungefragt mit jeder Adresse aus jeder Mail - genau daran ist das Altsystem gescheitert. Funde landen jetzt in einem Vorbereich (crm_leads.mail_pool = 1), der als Unterpunkt "Aus Mails" unter dem Menüpunkt Kontakte aufklappt. Der Kontaktbestand bleibt unberührt, bis dort jemand entscheidet; in Opportunities, Kalender und Netzwerk tauchen die Funde nicht auf. Eine Spalte statt einer zweiten Tabelle, weil crm_mail_links und crm_mail_contact_candidates per Fremdschlüssel auf crm_leads zeigen - so behält der Vorbereich seine Mailbezüge. Übernehmen kennt zwei Wege: - neue Person: Kopie mit mail_pool = 0, die Mailverknüpfungen wandern mit - Dublette (Adresse, sonst voller Name): gold markiert, abweichende Felder mit altem und neuem Wert; die Feldauswahl schreibt sie in den vorhandenen Kontakt. Vorausgewählt sind nur Lücken - ein Wert aus einer alten Signatur soll nichts Gepflegtes stillschweigend ersetzen. db.js verschiebt beim ersten Start die vorhandenen Kontakte mit source = 'Mail' in den Vorbereich, einmalig über den Merker leads_mail_pool_migrated in crm_settings. Die Sidebar-Gruppe ist dabei generisch geworden (MenuGroup) - Kontakte und Scanner benutzen jetzt denselben Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
366 lines
17 KiB
Markdown
366 lines
17 KiB
Markdown
# 11 Postfachanbindung (Microsoft 365)
|
|
|
|
Das gemeinsame Vertriebspostfach `crm@perinco.de` wird **lesend** in das CRM
|
|
gespiegelt. Aus jeder Mail entstehen Kontakt-Vorschlaege, und jede Mail laesst
|
|
sich an eine Opportunity und an Kontakte haengen.
|
|
|
|
Vorbild ist die gleiche Funktion im Altsystem KeepTrack. Uebernommen wurde die
|
|
Idee, nicht die Umsetzung - die Unterschiede stehen unten unter
|
|
[Was anders ist als im Altsystem](#was-anders-ist-als-im-altsystem) und sind der
|
|
Grund fuer mehrere Entwurfsentscheidungen.
|
|
|
|
## Ueberblick
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph M365["Microsoft 365"]
|
|
ENT["Entra ID<br/>App-Registrierung"]
|
|
GR["Microsoft Graph<br/>graph.microsoft.com"]
|
|
MB[("Postfach<br/>crm@perinco.de")]
|
|
GR --- MB
|
|
end
|
|
|
|
subgraph Server["perinco CRM Backend"]
|
|
SCH["mail/scheduler.js<br/>node-cron, Minutentakt"]
|
|
SY["mail/sync.js<br/>Delta Query"]
|
|
EX["mail/extract.js<br/>Kontakte lesen"]
|
|
ST["mail/store.js"]
|
|
CO["mail/contacts.js<br/>Uebernahme"]
|
|
API["routes/mail.js<br/>/api/mail"]
|
|
end
|
|
|
|
DB[("perinco_CRM<br/>crm_mail_*")]
|
|
SPA["Ansicht Mails"]
|
|
|
|
SCH --> SY
|
|
SY -->|"Token: client_credentials"| ENT
|
|
SY -->|"GET .../messages/delta"| GR
|
|
SY --> EX
|
|
EX --> ST
|
|
ST --> DB
|
|
EX --> CO
|
|
CO --> DB
|
|
SPA -->|"/api/mail/..."| API
|
|
API --> DB
|
|
API --> SY
|
|
```
|
|
|
|
## Was in Microsoft 365 eingerichtet sein muss
|
|
|
|
Genau **eine** Berechtigung, **ein** Zustimmungsklick, **eine**
|
|
Zugriffsrichtlinie. Kein Schreibrecht, kein Redirect-URI, kein interaktiver
|
|
Login.
|
|
|
|
1. **App-Registrierung** in Entra (Name z. B. `perinco CRM`), Kontotyp „nur
|
|
dieses Verzeichnis", Redirect-URI leer lassen. Anwendungs-ID und
|
|
Verzeichnis-ID notieren.
|
|
2. **Client-Geheimnis** anlegen und den Wert sofort kopieren.
|
|
3. **API-Berechtigung** Microsoft Graph → *Anwendungsberechtigungen* →
|
|
`Mail.Read`, danach Administratorzustimmung erteilen.
|
|
`Mail.ReadBasic` genuegt nicht, es liefert keinen Mailtext.
|
|
4. **Zugriff auf dieses eine Postfach begrenzen.** Anwendungsberechtigungen
|
|
gelten tenant-weit; ohne Richtlinie darf die App jedes Postfach lesen:
|
|
|
|
```powershell
|
|
Connect-ExchangeOnline -UserPrincipalName adm.sascha.wolf@perinco.onmicrosoft.com
|
|
|
|
New-ApplicationAccessPolicy -AppId <APP-ID> `
|
|
-PolicyScopeGroupId crm@perinco.de `
|
|
-AccessRight RestrictAccess `
|
|
-Description "perinco CRM: nur crm@perinco.de"
|
|
|
|
Test-ApplicationAccessPolicy -Identity crm@perinco.de -AppId <APP-ID> # Granted
|
|
Test-ApplicationAccessPolicy -Identity sascha.wolf@perinco.de -AppId <APP-ID> # Denied
|
|
```
|
|
|
|
`Add-MailboxPermission` ist hier **wirkungslos** - das ist der Weg fuer
|
|
delegierten Zugriff bzw. EWS-Impersonation. Bei App-only zaehlt allein die
|
|
Zugriffsrichtlinie.
|
|
|
|
Der Server muss ausgehend `login.microsoftonline.com` und `graph.microsoft.com`
|
|
auf 443 erreichen.
|
|
|
|
## Umgebungsvariablen
|
|
|
|
Zugangsdaten stehen ausschliesslich in `server/.env` - nie in der Datenbank und
|
|
nie in einer API-Antwort. Ein Datenbank-Dump enthaelt damit keine Geheimnisse.
|
|
|
|
| Variable | Pflicht | Bedeutung |
|
|
| --- | --- | --- |
|
|
| `MS_TENANT_ID` | ja | Verzeichnis-ID des Tenants |
|
|
| `MS_CLIENT_ID` | ja | Anwendungs-ID der App-Registrierung |
|
|
| `MS_CLIENT_SECRET` | ja | Client-Geheimnis |
|
|
| `MS_MAILBOX` | ja | abzugleichendes Postfach, Vorgabe `crm@perinco.de` |
|
|
| `MS_MAIL_FOLDER` | nein | Ordner, Vorgabe `inbox` |
|
|
| `MAIL_SYNC` | nein | `false` schaltet den Hintergrundlauf hart ab |
|
|
| `MAIL_MAX_PAGES` | nein | Delta-Seiten je Lauf, Vorgabe 20 |
|
|
| `MAIL_MAX_BODY_CHARS` | nein | Obergrenze je Mailtext, Vorgabe 400 000 |
|
|
|
|
Fehlt einer der Pflichtwerte, **startet das CRM normal**. Der Hintergrundlauf
|
|
bleibt aus, und die Einstellungen nennen die fehlende Variable beim Namen. Genau
|
|
wie beim Scanner soll eine unfertige Teilfunktion den Betrieb nicht verhindern.
|
|
|
|
Verstellbares liegt dagegen in `crm_settings` und ist unter *Einstellungen →
|
|
Postfach* erreichbar: `mail_sync_active`, `mail_sync_interval`,
|
|
`mail_auto_contacts`, `mail_since_days`, `mail_internal_domains`.
|
|
|
|
## Abgleich per Delta Query
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant S as scheduler.js
|
|
participant Y as sync.js
|
|
participant G as Graph
|
|
participant D as MariaDB
|
|
|
|
S->>D: last_run_at, has_more
|
|
Note over S: faellig? sonst nichts tun
|
|
S->>Y: runMailSync()
|
|
Y->>D: delta_link lesen
|
|
alt erster Lauf
|
|
Y->>G: /mailFolders/inbox/messages/delta?$filter=receivedDateTime ge ...
|
|
else spaeter
|
|
Y->>G: gespeicherter delta_link
|
|
end
|
|
G-->>Y: Seite mit Metadaten + nextLink/deltaLink
|
|
loop je Nachricht
|
|
Y->>D: bekannt? Text vorhanden?
|
|
Note over Y: unbekannt und aelter als die Grenze? dann weiter
|
|
opt Text fehlt
|
|
Y->>G: /messages/{id}?$select=body
|
|
end
|
|
Y->>D: upsert crm_mail_messages
|
|
opt neue Mail
|
|
Y->>D: Kontakt-Vorschlaege anlegen
|
|
opt gesichert und Automatik an
|
|
Y->>D: Kontakt anlegen bzw. ergaenzen
|
|
end
|
|
end
|
|
end
|
|
Y->>D: delta_link + has_more sichern
|
|
```
|
|
|
|
Wesentliche Eigenschaften:
|
|
|
|
* **Kein Schreibzugriff.** Der Fortschritt steht im `delta_link` in der
|
|
Datenbank, nicht im `isRead`-Kennzeichen des Postfachs. Deshalb genuegt
|
|
`Mail.Read`, und deshalb geht keine Mail verloren, wenn ein Mensch sie vorher
|
|
in Outlook oeffnet.
|
|
* **Fortsetzbar.** Nach jeder Seite wird der Verweis gesichert. Ein Absturz oder
|
|
ein Deployment mitten im Erstabgleich kostet hoechstens eine Seite.
|
|
* **Gedeckelt.** `MAIL_MAX_PAGES` begrenzt einen Lauf. Ist danach noch etwas
|
|
offen, setzt der naechste Lauf nach 15 Sekunden fort (`has_more`), statt das
|
|
normale Intervall abzuwarten.
|
|
* **Selbstheilend.** Antwortet Graph mit `410 Gone` bzw. `resyncRequired`, wird
|
|
der Verweis verworfen und beim naechsten Lauf neu aufgebaut. Vorhandene Mails
|
|
werden am SHA-256 der Graph-ID wiedererkannt, es entstehen keine Doppelten.
|
|
* **Nur Metadaten in der Delta-Abfrage.** Der Mailtext wird einzeln nachgeholt
|
|
und nur, wenn er fehlt. Ein reines Metadaten-Update (etwa ein geaendertes
|
|
Gelesen-Kennzeichen) loescht den gespeicherten Text nicht.
|
|
* **Zeitgrenze doppelt.** Der Erstabgleich versucht
|
|
`$filter=receivedDateTime ge ...`; die Filterunterstuetzung in Delta-Abfragen
|
|
ist eng begrenzt, bei einem 400 laeuft er ohne Filter weiter. Der Filter ist
|
|
deshalb nur eine Ersparnis - geprueft wird die Grenze **immer** beim Speichern,
|
|
und zwar nur fuer noch unbekannte Mails. Ein an den ersten Lauf gebundener
|
|
Merker waere hier falsch: ein ungefilterter Erstabgleich zieht sich ueber
|
|
mehrere Laeufe, und in den folgenden kaeme der Rest des Postfachs trotz Grenze
|
|
herein.
|
|
|
|
Im Postfach geloeschte Mails werden mit `is_deleted = 1` gekennzeichnet, nicht
|
|
entfernt: eine an eine Opportunity gehaengte Mail soll nicht aus dem CRM
|
|
verschwinden, weil jemand sein Postfach aufraeumt.
|
|
|
|
## Kontakte aus Mails
|
|
|
|
Die Extraktion arbeitet **ohne Sprachmodell**. Mailinhalte verlassen den Server
|
|
nicht.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
M["Mail"] --> A["Absender<br/>from (Graph)"]
|
|
M --> E["Empfaenger<br/>to, cc, bcc (Graph)"]
|
|
M --> S["Signaturblock<br/>Text nach der Grussformel"]
|
|
M --> T["Fliesstext<br/>Anrede + Name + Rufnummer"]
|
|
|
|
A --> F{"Filter"}
|
|
E --> F
|
|
S --> F
|
|
T --> F
|
|
|
|
F -->|"eigene Domain"| X1["verworfen"]
|
|
F -->|"info@, no-reply@, ..."| X2["verworfen"]
|
|
F -->|"Person"| C["Vorschlag mit Zuversicht"]
|
|
|
|
C --> Z{"E-Mail vorhanden<br/>und Zuversicht ≥ 70?"}
|
|
Z -->|ja, Automatik an| N["Eintrag in 'Aus Mails'<br/>mail_pool = 1"]
|
|
Z -->|"Adresse dort schon"| G["Eintrag ergaenzen"]
|
|
Z -->|nein| R["bleibt zur Durchsicht"]
|
|
```
|
|
|
|
Zuversicht je Herkunft: Absender 90 (95, wenn die Signatur die Adresse
|
|
bestaetigt), Empfaenger 78, weitere Adresse aus der Signatur 55, im Text genannte
|
|
Person 35.
|
|
|
|
Aus dem Signaturblock kommen Telefon und Mobil (nach Beschriftung
|
|
unterschieden), Firma (an der Rechtsform erkannt), Position, Strasse, PLZ, Ort,
|
|
Website und LinkedIn-Profil.
|
|
|
|
Zwei Entscheidungen, die die Trefferqualitaet tragen:
|
|
|
|
* **Im Fliesstext ist eine Anrede Pflicht** („Herrn Max Mustermann Tel. …").
|
|
Ohne diese Bedingung liest der Musterabgleich jede Folge grossgeschriebener
|
|
Woerter als Namen; „Zweiter Ansprechpartner" landete so als Kontakt. Ein
|
|
verpasster Treffer ist harmloser als ein falscher - eine Durchsichtsliste voll
|
|
Unsinn schaut niemand an.
|
|
* **Der Anzeigename schlaegt die Adresse.** Bei „Mueller" plus
|
|
`k.mueller@…` wird der Vorname aus der Adresse genommen, der Nachname aber aus
|
|
dem Anzeigenamen - dort stehen die Umlaute richtig.
|
|
|
|
## Vorbereich „Aus Mails"
|
|
|
|
Der Hintergrundlauf schreibt **nicht** in den Kontaktbestand. Was er findet,
|
|
landet in einem Vorbereich: `crm_leads.mail_pool = 1`. Die Kontaktansicht zeigt
|
|
nur `mail_pool = 0`, der Vorbereich steht als Unterpunkt *Aus Mails* unter dem
|
|
Menuepunkt *Kontakte*.
|
|
|
|
Der Grund ist Pflegbarkeit: die Kontaktliste ist eine gepflegte Liste, an der
|
|
Opportunities, Kalender und Netzwerk haengen. Ein Abgleich, der ungefragt jede
|
|
Adresse aus jeder Mail hineinschreibt, macht sie unbrauchbar - genau daran ist
|
|
das Altsystem gescheitert. Deshalb sind Funde aus Mails Rohmaterial, bis jemand
|
|
entscheidet.
|
|
|
|
Eine Spalte statt einer zweiten Tabelle, weil `crm_mail_links` und
|
|
`crm_mail_contact_candidates` per Fremdschluessel auf `crm_leads` zeigen: der
|
|
Vorbereich behaelt damit seine Mailbezuege, ohne dass es zwei Wege gibt, eine
|
|
Person mit einer Mail zu verbinden.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
P["Aus Mails<br/>mail_pool = 1"] --> D{"Person schon<br/>in den Kontakten?"}
|
|
D -->|nein| NEU["In Kontakte uebernehmen<br/>Kopie mit mail_pool = 0"]
|
|
D -->|"ja - Adresse oder Name"| GOLD["gold markiert<br/>Abweichungen hervorgehoben"]
|
|
GOLD --> M["Felder einzeln in den<br/>vorhandenen Kontakt uebernehmen"]
|
|
GOLD --> N2["trotzdem neu anlegen"]
|
|
P --> W["entfernen"]
|
|
```
|
|
|
|
Der Abgleich gegen die Kontakte laeuft im Browser (`mailPool.ts`): erst ueber die
|
|
E-Mail-Adresse, sonst ueber den vollen Namen. Telefonnummern werden dabei ueber
|
|
ihre Ziffern verglichen, damit „0221 123-4" und „02211234" nicht als Abweichung
|
|
erscheinen.
|
|
|
|
Beim Uebernehmen sind nur die **Luecken** vorausgewaehlt - Felder, in denen der
|
|
Kontakt noch nichts hat. Werte, die einen gepflegten Eintrag ersetzen wuerden,
|
|
sind als *ueberschreibt* gekennzeichnet und muessen bewusst angehakt werden: der
|
|
Mailfund kann aus einer alten Signatur stammen.
|
|
|
|
Uebernommene Kontakte tragen weiterhin `crm_leads.source = 'Mail'` - die
|
|
Kontaktliste hat dafuer den Herkunftsfilter *Aus Mails uebernommen*.
|
|
|
|
Bestandsumstellung: `db.js` verschiebt beim ersten Start alle vorhandenen
|
|
Kontakte mit `source = 'Mail'` in den Vorbereich. Das laeuft genau einmal,
|
|
festgehalten im Merker `leads_mail_pool_migrated` in `crm_settings` - sonst
|
|
wanderten uebernommene Kontakte bei jedem Neustart zurueck.
|
|
|
|
## Anzeige des Mailtexts
|
|
|
|
Fremdes HTML laeuft in einem `<iframe sandbox="">` **ohne jedes allow-Flag**. Das
|
|
Dokument bekommt eine eigene, undurchsichtige Origin und kann weder Skripte
|
|
ausfuehren noch auf die Anwendung zugreifen.
|
|
|
|
`allow-same-origin` waere hier ein Fehler: ein `srcDoc`-Dokument erbt damit die
|
|
Origin der Anwendung und koennte `localStorage` lesen - also das Anmeldetoken.
|
|
Weil die Hoehe des Inhalts ohne dieses Flag nicht auslesbar ist, hat der Rahmen
|
|
eine feste Hoehe mit eigenem Rollbalken und einen Schalter „Groesser".
|
|
|
|
Zusaetzlich entfernt das Backend beim Speichern Skripte, eingebettete Rahmen,
|
|
Formulare, Ereignis-Attribute und gefaehrliche URL-Schemata
|
|
(`mail/html.js`, `sanitizeHtml`).
|
|
|
|
Externe Bilder sind per Content-Security-Policy im eingebetteten Dokument
|
|
gesperrt und lassen sich pro Mail freigeben - andernfalls meldet jedes Oeffnen
|
|
dem Absender, dass die Mail gelesen wurde.
|
|
|
|
## Endpunkte
|
|
|
|
Alle unter `/api/mail`, alle hinter `requireAuth`.
|
|
|
|
| Methode | Pfad | Rolle | Zweck |
|
|
| --- | --- | --- | --- |
|
|
| GET | `/status` | alle | Betriebszustand, Zaehler, letzter Fehler |
|
|
| GET | `/messages` | alle | Liste; `search`, `filter`, `limit`, `offset` |
|
|
| GET | `/messages/:id` | alle | Mail mit Text, Zuordnungen, Vorschlaegen |
|
|
| POST | `/messages/:id/links` | alle | an Kontakt und/oder Opportunity haengen |
|
|
| DELETE | `/links/:id` | alle | Zuordnung loesen |
|
|
| GET | `/candidates` | alle | Kontakt-Vorschlaege, `status` waehlbar |
|
|
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergaenzen; `target: 'pool'` legt in „Aus Mails" ab |
|
|
| POST | `/candidates/:id/reject` | alle | Vorschlag verwerfen |
|
|
|
|
Der Vorbereich haengt an den Kontakten und hat seine Endpunkte deshalb unter
|
|
`/api/leads`:
|
|
|
|
| Methode | Pfad | Zweck |
|
|
| --- | --- | --- |
|
|
| GET | `/leads/mail-pool/sources` | Herkunft je Eintrag: Mailanzahl, letzter Betreff, Zuversicht |
|
|
| POST | `/leads/mail-pool/promote` | mehrere Eintraege als neue Kontakte anlegen |
|
|
| POST | `/leads/:id/promote` | einen Eintrag uebernehmen; mit `target_lead_id` und `fields` in einen vorhandenen Kontakt |
|
|
|
|
`keep_pool: true` behaelt den Eintrag in „Aus Mails"; ohne das verschwindet er
|
|
dort, sobald er als Kontakt gefuehrt wird. In beiden Faellen wandern die
|
|
Mailverknuepfungen mit an den Zielkontakt.
|
|
| GET | `/settings` | admin | verstellbare Einstellungen |
|
|
| POST | `/settings` | admin | Einstellungen speichern |
|
|
| POST | `/test` | admin | Anmeldung **und** Postfachzugriff pruefen |
|
|
| POST | `/sync` | admin | Abgleich anstossen, `{"reset":true}` von vorn |
|
|
|
|
Lesen ist fuer jeden Angemeldeten frei - `crm@perinco.de` ist ein gemeinsames
|
|
Vertriebspostfach. Konfigurieren, Testen und Anstossen sind Administratoren
|
|
vorbehalten.
|
|
|
|
`filter` in der Liste: `unlinked` (noch nirgends zugeordnet - die Arbeitsliste),
|
|
`candidates` (offene Kontakt-Vorschlaege), `linked` (bereits zugeordnet).
|
|
|
|
## Was anders ist als im Altsystem
|
|
|
|
| Thema | KeepTrack | perinco CRM |
|
|
| --- | --- | --- |
|
|
| Fortschritt | `$filter=isRead eq false`, danach `PATCH isRead` | Delta Query mit gespeichertem `delta_link` |
|
|
| Berechtigung | Schreibzugriff noetig | `Mail.Read` genuegt |
|
|
| Der Haken daran | `PATCH` ging auf `/me/…` - unter App-only gibt es kein `/me`. Der Aufruf lief in einen 400, die Antwort wurde nicht geprueft. Mails blieben ungelesen und wurden bei **jedem** Lauf erneut durch das Sprachmodell geschickt. | entfaellt |
|
|
| Tenant-/Client-ID | kamen vom Browser mit; jeder Angemeldete konnte das Server-Secret gegen einen fremden Tenant verwenden | serverseitig aus der `.env`, nie im Browser |
|
|
| Kontakte ohne Adresse | Platzhalter `name@kontakt-aus-email.local` erfunden, um eine Datenbankbedingung zu erfuellen | Kontakt ohne Adresse; Vorschlag bleibt zur Durchsicht liegen |
|
|
| Vorhandener Kontakt | wurde uebersprungen | leere Felder werden ergaenzt, gepflegte bleiben |
|
|
| Kontaktdaten lesen | kompletter Mailtext an OpenAI, auch fuer Daten, die Graph strukturiert liefert | Graph-Felder plus Signaturauswertung, kein externer Dienst |
|
|
| Mail am Deal | Text in das Freitextfeld `notizen` vorangestellt | eigene Tabelle `crm_mail_links`, loesbar, mehrfach moeglich |
|
|
| Verbindungstest | prueft nur, ob ein Token kommt | prueft Token **und** Postfachzugriff, mit Hinweis je Fehlercode |
|
|
| Paging | `@odata.nextLink` ignoriert, `$top=50` fest | Seiten werden gefolgt, je Lauf gedeckelt, fortsetzbar |
|
|
| HTML-Anzeige | ungefiltert | serverseitig entschaerft, `sandbox=""`, externe Bilder gesperrt |
|
|
| Zeitangaben | gemischt | durchgehend UTC in der Datenbank, Umrechnung im Frontend |
|
|
|
|
## Was noch nicht drin ist
|
|
|
|
* **Senden, Antworten, Weiterleiten.** Braucht `Mail.ReadWrite` und `Mail.Send`
|
|
und damit eine erneute Administratorzustimmung.
|
|
* **Anhaenge.** `has_attachments` wird gefuehrt und angezeigt, die Dateien
|
|
selbst werden nicht geholt.
|
|
* **Automatische Zuordnung zu Opportunities.** Die Zuordnung erfolgt per Hand;
|
|
der Dialog schlaegt anhand der Mailadressen Kontakt und Opportunity vor. Eine
|
|
Bewertung durch ein Sprachmodell waere der naechste Schritt - dann bitte mit
|
|
dem Schutz gegen Prompt-Injection, der im Altsystem in `mail-sync` fehlte.
|
|
* **Echtzeit statt Intervall.** Graph Change Notifications brauchen einen
|
|
oeffentlich erreichbaren Endpunkt (haben wir) und eine Erneuerung der
|
|
Subscription etwa alle drei Tage.
|
|
|
|
## Stoerungssuche
|
|
|
|
| Beobachtung | Ursache und Abhilfe |
|
|
| --- | --- |
|
|
| Einstellungen melden fehlende Variablen | `server/.env` ergaenzen, Dienst neu starten - die Werte werden beim Start gelesen |
|
|
| Test scheitert „bei der Anmeldung" | Tenant-ID, Client-ID oder Geheimnis falsch, oder das Geheimnis ist abgelaufen |
|
|
| Test scheitert „beim Postfachzugriff", `ErrorAccessDenied` | Administratorzustimmung fehlt, oder die ApplicationAccessPolicy erlaubt dieses Postfach nicht |
|
|
| Test scheitert mit `ResourceNotFound` | `MS_MAILBOX` zeigt auf ein Postfach, das es nicht gibt |
|
|
| „Erstabgleich laeuft noch" bleibt stehen | Log auf `[Mail]` pruefen; `MAIL_MAX_PAGES` erhoehen, wenn es zu langsam vorangeht |
|
|
| Keine Kontakt-Vorschlaege | stehen alle Absender auf einer eigenen Domain? `mail_internal_domains` pruefen |
|
|
| Doppelte Kontakte | tritt nicht auf: Abgleich erfolgt ueber die Adresse in Kleinschreibung; bei Treffer wird ergaenzt statt angelegt |
|
|
| Zaehler und Liste weichen ab | die Liste blendet im Postfach geloeschte Mails aus, der Zaehler in den Einstellungen ebenfalls - Zuordnungen bleiben trotzdem erhalten |
|