fix(postfach): Zeitgrenze gilt auch in Folgelaeufen des Erstabgleichs
Lehnt Graph den Serverfilter ab, laeuft der Erstabgleich ohne Filter - und zieht sich dann ueber mehrere Laeufe. Der Merker dafuer galt nur im ersten Lauf, in allen folgenden waere der Rest des Postfachs trotz eingestellter Grenze hereingekommen. Die Grenze wird jetzt immer geprueft, und zwar nur fuer noch unbekannte Mails: eine bereits gespiegelte Mail soll auch dann aktualisiert werden, wenn sie aus dem Zeitfenster gelaufen ist. insertCandidates() gibt die Zeilen jetzt in der Form der Datenbank zurueck. Die Umformung an der Aufrufstelle waere bei jedem neuen Feld still auseinandergelaufen und haette es bei der automatischen Uebernahme verloren. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6fd713497d
commit
8090d0aae1
+9
-4
@@ -125,6 +125,7 @@ sequenceDiagram
|
|||||||
G-->>Y: Seite mit Metadaten + nextLink/deltaLink
|
G-->>Y: Seite mit Metadaten + nextLink/deltaLink
|
||||||
loop je Nachricht
|
loop je Nachricht
|
||||||
Y->>D: bekannt? Text vorhanden?
|
Y->>D: bekannt? Text vorhanden?
|
||||||
|
Note over Y: unbekannt und aelter als die Grenze? dann weiter
|
||||||
opt Text fehlt
|
opt Text fehlt
|
||||||
Y->>G: /messages/{id}?$select=body
|
Y->>G: /messages/{id}?$select=body
|
||||||
end
|
end
|
||||||
@@ -156,10 +157,14 @@ Wesentliche Eigenschaften:
|
|||||||
* **Nur Metadaten in der Delta-Abfrage.** Der Mailtext wird einzeln nachgeholt
|
* **Nur Metadaten in der Delta-Abfrage.** Der Mailtext wird einzeln nachgeholt
|
||||||
und nur, wenn er fehlt. Ein reines Metadaten-Update (etwa ein geaendertes
|
und nur, wenn er fehlt. Ein reines Metadaten-Update (etwa ein geaendertes
|
||||||
Gelesen-Kennzeichen) loescht den gespeicherten Text nicht.
|
Gelesen-Kennzeichen) loescht den gespeicherten Text nicht.
|
||||||
* **Zeitfilter mit Ersatzweg.** Der Erstabgleich versucht
|
* **Zeitgrenze doppelt.** Der Erstabgleich versucht
|
||||||
`$filter=receivedDateTime ge ...`. Die Filterunterstuetzung in Delta-Abfragen
|
`$filter=receivedDateTime ge ...`; die Filterunterstuetzung in Delta-Abfragen
|
||||||
ist eng begrenzt; lehnt Graph ab (400), laeuft der Abgleich ohne Filter und die
|
ist eng begrenzt, bei einem 400 laeuft er ohne Filter weiter. Der Filter ist
|
||||||
Zeitgrenze greift beim Speichern.
|
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
|
Im Postfach geloeschte Mails werden mit `is_deleted = 1` gekennzeichnet, nicht
|
||||||
entfernt: eine an eine Opportunity gehaengte Mail soll nicht aus dem CRM
|
entfernt: eine an eine Opportunity gehaengte Mail soll nicht aus dem CRM
|
||||||
|
|||||||
+26
-22
@@ -158,39 +158,43 @@ const CANDIDATE_COLUMNS = [
|
|||||||
/**
|
/**
|
||||||
* Vorschlaege zu einer Mail speichern. INSERT IGNORE zusammen mit dem
|
* Vorschlaege zu einer Mail speichern. INSERT IGNORE zusammen mit dem
|
||||||
* Unique-Schluessel (message_id, dedupe_hash) macht den Aufruf wiederholbar.
|
* Unique-Schluessel (message_id, dedupe_hash) macht den Aufruf wiederholbar.
|
||||||
* Zurueck kommen die tatsaechlich neu angelegten Zeilen.
|
*
|
||||||
|
* Zurueck kommen die tatsaechlich neu angelegten Zeilen - und zwar in der Form
|
||||||
|
* der Datenbank (snake_case), damit sie direkt an adoptCandidate() weitergegeben
|
||||||
|
* werden koennen. Eine Umformung an der Aufrufstelle wuerde bei jedem neuen Feld
|
||||||
|
* still auseinanderlaufen.
|
||||||
*/
|
*/
|
||||||
export async function insertCandidates(messageId, candidates) {
|
export async function insertCandidates(messageId, candidates) {
|
||||||
const created = [];
|
const created = [];
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const values = [
|
const row = {
|
||||||
messageId,
|
message_id: messageId,
|
||||||
candidate.dedupeHash,
|
dedupe_hash: candidate.dedupeHash,
|
||||||
candidate.firstName,
|
first_name: candidate.firstName,
|
||||||
candidate.lastName,
|
last_name: candidate.lastName,
|
||||||
candidate.salutation,
|
salutation: candidate.salutation,
|
||||||
candidate.email,
|
email: candidate.email,
|
||||||
candidate.phone,
|
phone: candidate.phone,
|
||||||
candidate.mobile,
|
mobile: candidate.mobile,
|
||||||
candidate.company,
|
company: candidate.company,
|
||||||
candidate.position,
|
position: candidate.position,
|
||||||
candidate.street,
|
street: candidate.street,
|
||||||
candidate.zip,
|
zip: candidate.zip,
|
||||||
candidate.city,
|
city: candidate.city,
|
||||||
candidate.website,
|
website: candidate.website,
|
||||||
candidate.linkedin,
|
linkedin: candidate.linkedin,
|
||||||
candidate.origin,
|
origin: candidate.origin,
|
||||||
candidate.confidence,
|
confidence: candidate.confidence,
|
||||||
];
|
};
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT IGNORE INTO crm_mail_contact_candidates (${CANDIDATE_COLUMNS.join(', ')})
|
`INSERT IGNORE INTO crm_mail_contact_candidates (${CANDIDATE_COLUMNS.join(', ')})
|
||||||
VALUES (${CANDIDATE_COLUMNS.map(() => '?').join(', ')})`,
|
VALUES (${CANDIDATE_COLUMNS.map(() => '?').join(', ')})`,
|
||||||
values
|
CANDIDATE_COLUMNS.map((column) => row[column])
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0) created.push({ id: result.insertId, ...candidate });
|
if (result.affectedRows > 0) created.push({ id: result.insertId, ...row });
|
||||||
}
|
}
|
||||||
|
|
||||||
return created;
|
return created;
|
||||||
|
|||||||
+25
-39
@@ -136,16 +136,16 @@ function initialDeltaUrl(since) {
|
|||||||
/**
|
/**
|
||||||
* Delta-Seite holen. Der erste Aufruf versucht es mit Zeitfilter; lehnt Graph den
|
* Delta-Seite holen. Der erste Aufruf versucht es mit Zeitfilter; lehnt Graph den
|
||||||
* ab (die Filterunterstuetzung in Delta-Abfragen ist eng begrenzt), wird ohne
|
* ab (die Filterunterstuetzung in Delta-Abfragen ist eng begrenzt), wird ohne
|
||||||
* Filter wiederholt und der Zeitraum stattdessen beim Speichern beachtet.
|
* Filter wiederholt. Der Filter ist damit reine Ersparnis - die Zeitgrenze wird
|
||||||
|
* beim Speichern in jedem Fall geprueft.
|
||||||
*/
|
*/
|
||||||
async function fetchDeltaPage(url, { allowFilterFallback = false, since = null } = {}) {
|
async function fetchDeltaPage(url, { allowFilterFallback = false } = {}) {
|
||||||
try {
|
try {
|
||||||
return { data: await graphRequest(url), url };
|
return await graphRequest(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (allowFilterFallback && err instanceof GraphError && err.status === 400) {
|
if (allowFilterFallback && err instanceof GraphError && err.status === 400) {
|
||||||
console.warn('[Mail] Delta mit Zeitfilter abgelehnt - wiederhole ohne Filter.');
|
console.warn('[Mail] Delta mit Zeitfilter abgelehnt - wiederhole ohne Filter.');
|
||||||
const fallback = initialDeltaUrl(null);
|
return graphRequest(initialDeltaUrl(null));
|
||||||
return { data: await graphRequest(fallback), url: fallback, filtered: false, since };
|
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@@ -182,21 +182,12 @@ export async function runMailSync({ trigger = 'manuell' } = {}) {
|
|||||||
const cutoffSql = toSqlUtc(cutoff.toISOString());
|
const cutoffSql = toSqlUtc(cutoff.toISOString());
|
||||||
|
|
||||||
let url = state.delta_link || initialDeltaUrl(cutoff.toISOString().replace(/\.\d{3}Z$/, 'Z'));
|
let url = state.delta_link || initialDeltaUrl(cutoff.toISOString().replace(/\.\d{3}Z$/, 'Z'));
|
||||||
// Wird nur gesetzt, wenn Graph den Zeitfilter ablehnt und wir ohne Filter
|
|
||||||
// durchlaufen - dann muss die Zeitgrenze hier greifen.
|
|
||||||
let applyCutoffLocally = false;
|
|
||||||
let deltaLink = null;
|
let deltaLink = null;
|
||||||
let hasMore = false;
|
let hasMore = false;
|
||||||
|
|
||||||
while (url && pages < MAX_PAGES_PER_RUN) {
|
while (url && pages < MAX_PAGES_PER_RUN) {
|
||||||
const page = await fetchDeltaPage(url, {
|
const data = (await fetchDeltaPage(url, { allowFilterFallback: isFirstRun && pages === 0 })) ?? {};
|
||||||
allowFilterFallback: isFirstRun && pages === 0,
|
|
||||||
since: cutoff.toISOString(),
|
|
||||||
});
|
|
||||||
if (page.filtered === false) applyCutoffLocally = true;
|
|
||||||
|
|
||||||
pages += 1;
|
pages += 1;
|
||||||
const data = page.data ?? {};
|
|
||||||
|
|
||||||
for (const raw of data.value || []) {
|
for (const raw of data.value || []) {
|
||||||
if (!raw?.id) continue;
|
if (!raw?.id) continue;
|
||||||
@@ -208,11 +199,20 @@ export async function runMailSync({ trigger = 'manuell' } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = normalizeMessage(raw);
|
const message = normalizeMessage(raw);
|
||||||
|
|
||||||
// Beim Ersatzweg ohne Zeitfilter greift die Grenze hier.
|
|
||||||
if (applyCutoffLocally && cutoffSql && message.receivedAt < cutoffSql) continue;
|
|
||||||
|
|
||||||
const existing = await getMessageStateByHash(message.graphIdHash);
|
const existing = await getMessageStateByHash(message.graphIdHash);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Zeitgrenze - immer geprueft, nicht nur wenn der Serverfilter versagt
|
||||||
|
* hat. Ein Erstabgleich ohne Filter zieht sich ueber mehrere Laeufe; in
|
||||||
|
* den folgenden waere ein an den ersten Lauf gebundenes Merker-Flag
|
||||||
|
* verloren, und der Rest des Postfachs kaeme trotz Grenze herein.
|
||||||
|
*
|
||||||
|
* Nur fuer unbekannte Mails: eine bereits gespiegelte Mail soll auch dann
|
||||||
|
* aktualisiert werden, wenn sie inzwischen aus dem Zeitfenster gelaufen
|
||||||
|
* ist.
|
||||||
|
*/
|
||||||
|
if (!existing && cutoffSql && message.receivedAt < cutoffSql) continue;
|
||||||
|
|
||||||
if (!existing || !existing.hasBody) {
|
if (!existing || !existing.hasBody) {
|
||||||
try {
|
try {
|
||||||
Object.assign(message, await loadBody(message.graphId));
|
Object.assign(message, await loadBody(message.graphId));
|
||||||
@@ -236,31 +236,17 @@ export async function runMailSync({ trigger = 'manuell' } = {}) {
|
|||||||
const created = await insertCandidates(stored.id, candidates);
|
const created = await insertCandidates(stored.id, candidates);
|
||||||
newCandidates += created.length;
|
newCandidates += created.length;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Automatisch uebernommen wird nur, wer eine echte Adresse hat und aus
|
||||||
|
* Absender oder Empfaengerliste stammt. Signatur- und Textfunde liegen
|
||||||
|
* unter der Schwelle und warten auf eine Entscheidung.
|
||||||
|
*/
|
||||||
if (settings.autoContacts) {
|
if (settings.autoContacts) {
|
||||||
for (const candidate of created) {
|
for (const candidate of created) {
|
||||||
if (!candidate.email) continue;
|
if (!candidate.email) continue;
|
||||||
if (candidate.confidence < AUTO_ADOPT_MIN_CONFIDENCE) continue;
|
if (candidate.confidence < AUTO_ADOPT_MIN_CONFIDENCE) continue;
|
||||||
try {
|
try {
|
||||||
const result = await adoptCandidate(
|
const result = await adoptCandidate(candidate, { mode: 'automatisch' });
|
||||||
{
|
|
||||||
id: candidate.id,
|
|
||||||
message_id: stored.id,
|
|
||||||
first_name: candidate.firstName,
|
|
||||||
last_name: candidate.lastName,
|
|
||||||
salutation: candidate.salutation,
|
|
||||||
email: candidate.email,
|
|
||||||
phone: candidate.phone,
|
|
||||||
mobile: candidate.mobile,
|
|
||||||
company: candidate.company,
|
|
||||||
position: candidate.position,
|
|
||||||
street: candidate.street,
|
|
||||||
zip: candidate.zip,
|
|
||||||
city: candidate.city,
|
|
||||||
website: candidate.website,
|
|
||||||
linkedin: candidate.linkedin,
|
|
||||||
},
|
|
||||||
{ mode: 'automatisch' }
|
|
||||||
);
|
|
||||||
if (result.status === 'uebernommen') adoptedContacts += 1;
|
if (result.status === 'uebernommen') adoptedContacts += 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[Mail] Kontakt-Vorschlag konnte nicht uebernommen werden: ${err.message}`);
|
console.warn(`[Mail] Kontakt-Vorschlag konnte nicht uebernommen werden: ${err.message}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user