diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..79e1496f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +Alle nennenswerten Aenderungen an Starface Outlook Sync werden hier dokumentiert. + +Format orientiert sich an [Keep a Changelog](https://keepachangelog.com/de/), +Versionsschema ist `x.x.x.x` (siehe `release.sh`). + +## [Unreleased] + +### Behoben + +- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende + Ursachen wurden beseitigt: + - Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen + Lade-Fehler oder Timeout) liess die Engine Kontakte faelschlich als + "geloescht" ansehen, ihr Mapping verwerfen und sie beim naechsten Lauf neu + anlegen. Der Kontakt-Abruf bricht jetzt mit Fehlermeldung ab (inkl. + Wiederholversuch), statt still mit einer Teil-Liste weiterzuarbeiten. + - Ist ein Starface-Kontakt nicht in der geladenen Liste (z.B. anderes + Adressbuch), wird das Mapping jetzt **behalten** statt verworfen und neu + angelegt. + - Das Wiederzuordnen bestehender Kontakte war zu streng: eine von Starface + umformatierte Telefonnummer konnte einen eindeutigen E-Mail- oder + Namens-Treffer ueberstimmen und so eine Neuanlage statt Verknuepfung + ausloesen. Ein gleicher E-Mail- oder voller Namens-Treffer reicht jetzt. + +- **Synchronisation extrem langsam / schrieb bei jedem Lauf alle Kontakte neu.** + Die Aenderungserkennung verglich einen einzigen Hash gegen beide Seiten. + Outlook und Starface stellen denselben Kontakt aber unterschiedlich dar + (nicht uebertragene Felder, abweichendes Telefonformat), wodurch der Hash nie + uebereinstimmte und praktisch jeder Kontakt bei jedem Sync neu geschrieben + wurde. Jede Seite hat jetzt eine eigene Baseline (`LastOutlookHash` / + `LastStarfaceHash`); nur tatsaechlich geaenderte Kontakte werden geschrieben. + +### Geaendert + +- Starface-Kontaktdetails werden beim Laden parallel abgerufen (8 gleichzeitig) + statt einzeln nacheinander – deutlich schneller bei grossen Adressbuechern. +- `UpdateContact` (Outlook) und `UpdateContactAsync` (Starface) geben jetzt den + frisch eingelesenen Stand zurueck, damit die Baseline nach dem Schreiben + korrekt gesetzt wird und der Sync sauber konvergiert. + +### Migration + +- Bestehende Mapping-Dateien werden beim ersten Sync automatisch uebernommen + (ein ruhiger Durchlauf ohne Massen-Update). +- **Bereits vorhandene Dubletten werden nicht automatisch entfernt** – die Fixes + verhindern nur neue. Vorhandene Doppel-Kontakte einmalig manuell bereinigen. + +## [0.0.0.23] + +### Behoben + +- Kritischer Loesch-Fehler: Kontakte wurden bei einer geaenderten Outlook-EntryID + nicht mehr massenhaft geloescht. + +### Geaendert + +- Outlook-Sicherheitsschluessel werden fuer Domaenenumgebungen zusaetzlich nach + HKLM geschrieben (unterdrueckt die Outlook-Sicherheitsabfrage). + +## Aeltere Versionen + +Aeltere Releases (v0.0.0.1 – v0.0.0.22) sind ueber die Git-Historie und die +Git-Tags nachvollziehbar. diff --git a/src/StarfaceOutlookSync/Models/SyncProfile.cs b/src/StarfaceOutlookSync/Models/SyncProfile.cs index 57b2a493..ea196cd3 100644 --- a/src/StarfaceOutlookSync/Models/SyncProfile.cs +++ b/src/StarfaceOutlookSync/Models/SyncProfile.cs @@ -45,6 +45,15 @@ namespace StarfaceOutlookSync.Models public string ProfileId { get; set; } = ""; public string OutlookEntryId { get; set; } = ""; public string StarfaceId { get; set; } = ""; + + // Getrennte Baselines pro Seite. Outlook und Starface stellen denselben + // Kontakt unterschiedlich dar (Felder, Telefonformat), daher MUSS jede + // Seite gegen ihre eigene zuletzt-gesehene Repraesentation verglichen + // werden - sonst gilt jeder Kontakt bei jedem Sync als geaendert. + public string LastOutlookHash { get; set; } = ""; + public string LastStarfaceHash { get; set; } = ""; + + // Alt-Feld (vor v0.0.0.24). Nur noch fuer Migration bestehender Mappings. public string LastSyncHash { get; set; } = ""; } diff --git a/src/StarfaceOutlookSync/Services/OutlookContactsService.cs b/src/StarfaceOutlookSync/Services/OutlookContactsService.cs index 9259869c..e5820b78 100644 --- a/src/StarfaceOutlookSync/Services/OutlookContactsService.cs +++ b/src/StarfaceOutlookSync/Services/OutlookContactsService.cs @@ -350,7 +350,13 @@ namespace StarfaceOutlookSync.Services } } - public bool UpdateContact(string entryId, UnifiedContact contact) + /// + /// Aktualisiert den Outlook-Kontakt und gibt den frisch eingelesenen + /// Stand zurueck (null bei Fehler). Der zurueckgegebene Kontakt liefert + /// den massgeblichen Hash NACH dem Schreiben - noetig damit die naechste + /// Synchronisation den Kontakt nicht erneut als geaendert erkennt. + /// + public UnifiedContact UpdateContact(string entryId, UnifiedContact contact) { try { @@ -361,15 +367,17 @@ namespace StarfaceOutlookSync.Services MapToOutlook(contact, ci); ci.Save(); + var updated = MapFromOutlook(ci); + Marshal.ReleaseComObject(ci); Marshal.ReleaseComObject(ns); - return true; + return updated; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error updating contact: {ex.Message}"); - return false; + return null; } } diff --git a/src/StarfaceOutlookSync/Services/StarfaceApiClient.cs b/src/StarfaceOutlookSync/Services/StarfaceApiClient.cs index 21234fa5..19808248 100644 --- a/src/StarfaceOutlookSync/Services/StarfaceApiClient.cs +++ b/src/StarfaceOutlookSync/Services/StarfaceApiClient.cs @@ -194,7 +194,16 @@ namespace StarfaceOutlookSync.Services query += $"&tags={book.TagId}"; var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}"); - if (!resp.IsSuccessStatusCode) break; + if (!resp.IsSuccessStatusCode) + { + // WICHTIG: nicht still abbrechen. Eine unvollstaendige Liste + // laesst die Sync-Engine Kontakte faelschlich als geloescht + // ansehen -> Mappings werden verworfen -> Dubletten. + throw new Exception( + $"Starface-Kontaktliste konnte nicht vollstaendig geladen werden " + + $"(Seite {page}: HTTP {(int)resp.StatusCode}). Synchronisation abgebrochen, " + + $"um Dubletten zu vermeiden."); + } var body = await resp.Content.ReadAsStringAsync(); JArray array; @@ -232,31 +241,40 @@ namespace StarfaceOutlookSync.Services OnDebug?.Invoke($"Seite {page}: {array.Count} Kontakte in Liste"); - // Die Listen-API gibt nur Summary zurueck. - // Jeden Kontakt einzeln abrufen fuer alle Felder. - foreach (var item in array) + if (firstPage) { - var id = item["id"]?.ToString(); - if (string.IsNullOrEmpty(id)) continue; - - try + var firstId = array[0]?["id"]?.ToString(); + if (!string.IsNullOrEmpty(firstId)) { - var detailResp = await _http.GetAsync($"{_baseUrl}/contacts/{id}"); - if (detailResp.IsSuccessStatusCode) - { - var detailBody = await detailResp.Content.ReadAsStringAsync(); - var detailObj = JObject.Parse(detailBody); - - if (firstPage) - { - OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{detailObj.ToString(Formatting.Indented)}"); - firstPage = false; - } - - contacts.Add(MapFromStarface(detailObj)); - } + var sample = await FetchDetailAsync(firstId); + if (sample != null) + OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{sample.ToString(Formatting.Indented)}"); } - catch { } + firstPage = false; + } + + // Die Listen-API liefert nur eine Zusammenfassung; jeder Kontakt + // muss einzeln geladen werden. Das parallelisieren (begrenzt), + // sonst dauert es bei vielen Kontakten extrem lange. + var ids = array + .Select(it => it["id"]?.ToString()) + .Where(id => !string.IsNullOrEmpty(id)) + .ToList(); + + const int maxParallel = 8; + for (int i = 0; i < ids.Count; i += maxParallel) + { + var batch = ids.Skip(i).Take(maxParallel) + .Select(async id => + { + var detail = await FetchDetailAsync(id); + // null = 404 (zwischenzeitlich geloescht) -> ueberspringen. + return detail == null ? null : MapFromStarface(detail); + }) + .ToList(); + + var mapped = await Task.WhenAll(batch); + contacts.AddRange(mapped.Where(c => c != null)); } if (array.Count < pageSize) break; @@ -266,6 +284,36 @@ namespace StarfaceOutlookSync.Services return contacts; } + /// + /// Laedt das Detail-JSON eines Kontakts mit kleiner Wiederholung. + /// Gibt null zurueck, wenn der Kontakt zwischen Listen- und Detail-Abruf + /// wirklich geloescht wurde (404 - harmlos, wird uebersprungen). + /// Wirft bei transienten Fehlern, damit der Aufrufer NICHT mit einer + /// unvollstaendigen Liste weiterarbeitet (sonst entstehen Dubletten). + /// + private async Task FetchDetailAsync(string id) + { + for (int attempt = 0; attempt < 3; attempt++) + { + try + { + var resp = await _http.GetAsync($"{_baseUrl}/contacts/{id}"); + if (resp.IsSuccessStatusCode) + return JObject.Parse(await resp.Content.ReadAsStringAsync()); + + // 404 = zwischenzeitlich geloescht; nicht erneut versuchen. + if (resp.StatusCode == HttpStatusCode.NotFound) return null; + } + catch { } + + await Task.Delay(250 * (attempt + 1)); + } + + throw new Exception( + $"Starface-Kontakt {id} konnte nach mehreren Versuchen nicht geladen werden. " + + $"Synchronisation abgebrochen, um Dubletten zu vermeiden."); + } + public async Task CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book) { var sfContact = MapToStarface(contact); @@ -300,7 +348,13 @@ namespace StarfaceOutlookSync.Services return MapFromStarface(created); } - public async Task UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book) + /// + /// Aktualisiert den Starface-Kontakt und gibt den massgeblichen Stand + /// NACH dem Schreiben zurueck (null bei Fehler). Wird fuer die getrennte + /// Hash-Baseline benoetigt, damit der Kontakt beim naechsten Sync nicht + /// erneut faelschlich als geaendert gilt. + /// + public async Task UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book) { var sfContact = MapToStarface(contact); sfContact["id"] = contactId; @@ -323,9 +377,37 @@ namespace StarfaceOutlookSync.Services { var respBody = await resp.Content.ReadAsStringAsync(); OnDebug?.Invoke($"PUT /contacts/{contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}"); + return null; } - return resp.IsSuccessStatusCode; + // Frischen Stand zurueckgeben. Manche Versionen liefern den Kontakt + // direkt in der PUT-Antwort, sonst per GET nachladen. + try + { + var respBody = await resp.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(respBody)) + { + var obj = JObject.Parse(respBody); + if (obj["blocks"] != null) + return MapFromStarface(obj); + } + } + catch { } + + return await GetContactAsync(contactId) ?? contact; + } + + /// Laedt einen einzelnen Kontakt mit allen Feldern. + public async Task GetContactAsync(string contactId) + { + try + { + var resp = await _http.GetAsync($"{_baseUrl}/contacts/{contactId}"); + if (!resp.IsSuccessStatusCode) return null; + var obj = JObject.Parse(await resp.Content.ReadAsStringAsync()); + return MapFromStarface(obj); + } + catch { return null; } } public async Task DeleteContactAsync(string contactId) diff --git a/src/StarfaceOutlookSync/Services/SyncEngine.cs b/src/StarfaceOutlookSync/Services/SyncEngine.cs index adcb5285..b2b87361 100644 --- a/src/StarfaceOutlookSync/Services/SyncEngine.cs +++ b/src/StarfaceOutlookSync/Services/SyncEngine.cs @@ -37,72 +37,38 @@ namespace StarfaceOutlookSync.Services private static bool IsMatch(UnifiedContact a, UnifiedContact b) { - // Mindestens ein identifizierendes Feld muss vorhanden sein - bool hasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName); - bool hasEmail = !string.IsNullOrEmpty(a.Email); - bool hasPhone = !string.IsNullOrEmpty(a.PhoneWork) || !string.IsNullOrEmpty(a.PhoneMobile); + bool hasName = (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName)) + && (!string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName)); - if (!hasName && !hasEmail && !hasPhone) return false; - - // E-Mail: wenn auf beiden Seiten vorhanden, muss sie gleich sein - // Wenn nur auf einer Seite vorhanden -> kein Match - if (!FieldsCompatible(a.Email, b.Email)) return false; - - // Name: wenn auf einer Seite vorhanden, muss er gleich sein - if (!FieldsCompatible(a.FirstName, b.FirstName)) return false; - if (!FieldsCompatible(a.LastName, b.LastName)) return false; - - // Firma: wenn auf einer Seite vorhanden, muss sie gleich sein - // Leere Firma vs. gefuellte Firma = verschiedene Kontakte - if (!FieldsCompatible(a.Company, b.Company)) return false; - - // Telefon/Fax: wenn auf einer Seite vorhanden, muss es gleich sein - if (!PhoneFieldsCompatible(a.PhoneWork, b.PhoneWork)) return false; - if (!PhoneFieldsCompatible(a.PhoneMobile, b.PhoneMobile)) return false; - if (!PhoneFieldsCompatible(a.PhoneHome, b.PhoneHome)) return false; - if (!PhoneFieldsCompatible(a.Fax, b.Fax)) return false; - - // Mindestens ein starkes Match muss vorhanden sein + // Starke Identifikatoren bool emailMatch = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase); bool nameMatch = hasName - && a.FirstName.Equals(b.FirstName, StringComparison.OrdinalIgnoreCase) - && a.LastName.Equals(b.LastName, StringComparison.OrdinalIgnoreCase) - && (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName)); + && (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase) + && (a.LastName ?? "").Equals(b.LastName ?? "", StringComparison.OrdinalIgnoreCase); bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork) && NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork)) + || (!string.IsNullOrEmpty(a.PhoneMobile) && !string.IsNullOrEmpty(b.PhoneMobile) + && NormalizePhone(a.PhoneMobile) == NormalizePhone(b.PhoneMobile)) || (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax) && NormalizePhone(a.Fax) == NormalizePhone(b.Fax)); bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company) && a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase); - // Email oder Name reicht. Telefon/Fax nur mit Firma zusammen. - return emailMatch || nameMatch || (phoneMatch && companyMatch) || (companyMatch && phoneMatch); - } + // Widerspruch: beide haben eine E-Mail, aber unterschiedlich -> verschiedene Personen. + bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch; - /// - /// Prueft ob zwei Felder kompatibel sind. - /// Beide leer = kompatibel. Beide gleich = kompatibel. - /// Eins leer, eins gefuellt = NICHT kompatibel (verschiedene Kontakte). - /// - private static bool FieldsCompatible(string a, string b) - { - bool aEmpty = string.IsNullOrEmpty(a); - bool bEmpty = string.IsNullOrEmpty(b); + // Gleiche E-Mail ist der staerkste Identifikator und reicht allein. + if (emailMatch) return true; - if (aEmpty && bEmpty) return true; - if (aEmpty != bEmpty) return false; // Einer leer, anderer nicht - return a.Equals(b, StringComparison.OrdinalIgnoreCase); - } + // Gleicher voller Name reicht, solange keine widerspruechliche E-Mail vorliegt. + // (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.) + if (nameMatch && !emailContradiction) return true; - private static bool PhoneFieldsCompatible(string a, string b) - { - bool aEmpty = string.IsNullOrEmpty(a); - bool bEmpty = string.IsNullOrEmpty(b); + // Schwacher Pfad: Telefon/Fax nur zusammen mit gleicher Firma und ohne E-Mail-Widerspruch. + if (phoneMatch && companyMatch && !emailContradiction) return true; - if (aEmpty && bEmpty) return true; - if (aEmpty != bEmpty) return false; - return NormalizePhone(a) == NormalizePhone(b); + return false; } private static string NormalizePhone(string phone) @@ -215,29 +181,50 @@ namespace StarfaceOutlookSync.Services if (oc != null && sc == null) { - // Starface-Kontakt nicht gefunden. - // Kann passieren wenn der Kontakt einem anderen Adressbuch gehoert. - // NICHT loeschen, nur Mapping entfernen - wird in Phase 2/3 neu verknuepft - Log($" Starface-Kontakt nicht in Liste (anderes Adressbuch?): {oc.DisplayName}"); - // Mapping verwerfen, Outlook-Kontakt als unverarbeitet belassen - // damit er in Phase 2 neu zugeordnet oder erstellt werden kann + // Starface-Kontakt nicht in der geladenen Liste. + // Da unvollstaendige Ladevorgaenge inzwischen abgebrochen + // werden (siehe StarfaceApiClient), liegt das hoechstens an + // einem anderen Adressbuch. NICHT loeschen und NICHT neu + // anlegen - sonst entstehen Dubletten. Mapping behalten, + // beim naechsten Sync wird es erneut abgeglichen. + Log($" Starface-Kontakt nicht in Liste (anderes Adressbuch?), behalte Mapping: {oc.DisplayName}"); + newMappings.Add(mapping); continue; } if (oc != null && sc != null) { - // Beide vorhanden -> auf Aenderungen pruefen + // Beide vorhanden -> auf Aenderungen pruefen. + // WICHTIG: jede Seite gegen ihre EIGENE Baseline pruefen. + // Outlook und Starface stellen denselben Kontakt + // unterschiedlich dar, ein gemeinsamer Hash schlaegt nie an. var olHash = oc.GetHash(); var sfHash = sc.GetHash(); - bool olChanged = olHash != mapping.LastSyncHash; - bool sfChanged = sfHash != mapping.LastSyncHash; + + // Migration alter Mappings (nur LastSyncHash vorhanden): + // aktuellen Stand als Baseline uebernehmen und als synchron + // annehmen, damit kein Massen-Update ausgeloest wird. + if (string.IsNullOrEmpty(mapping.LastOutlookHash) && + string.IsNullOrEmpty(mapping.LastStarfaceHash)) + { + mapping.LastOutlookHash = olHash; + mapping.LastStarfaceHash = sfHash; + mapping.LastSyncHash = ""; + newMappings.Add(mapping); + continue; + } + + bool olChanged = olHash != mapping.LastOutlookHash; + bool sfChanged = sfHash != mapping.LastStarfaceHash; if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)) { // Outlook hat sich geaendert -> Starface updaten - if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) + var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook); + if (updated != null) { - mapping.LastSyncHash = olHash; + mapping.LastOutlookHash = olHash; + mapping.LastStarfaceHash = updated.GetHash(); result.Updated++; Log($" Aktualisiert (OL->SF): {oc.DisplayName}"); } @@ -245,30 +232,36 @@ namespace StarfaceOutlookSync.Services else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)) { // Starface hat sich geaendert -> Outlook updaten - if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) + var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc); + if (updated != null) { - mapping.LastSyncHash = sfHash; + mapping.LastStarfaceHash = sfHash; + mapping.LastOutlookHash = updated.GetHash(); result.Updated++; Log($" Aktualisiert (SF->OL): {sc.DisplayName}"); } } else if (olChanged && sfChanged) { - // Beide geaendert -> Konflikt, neuere gewinnt (Outlook bevorzugt) + // Beide geaendert -> Konflikt, Outlook bevorzugt if (profile.SyncDirection != SyncDirection.StarfaceToOutlook) { - if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) + var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook); + if (updated != null) { - mapping.LastSyncHash = olHash; + mapping.LastOutlookHash = olHash; + mapping.LastStarfaceHash = updated.GetHash(); result.Updated++; Log($" Konflikt (OL gewinnt): {oc.DisplayName}"); } } else { - if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) + var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc); + if (updated != null) { - mapping.LastSyncHash = sfHash; + mapping.LastStarfaceHash = sfHash; + mapping.LastOutlookHash = updated.GetHash(); result.Updated++; Log($" Konflikt (SF gewinnt): {sc.DisplayName}"); } @@ -306,14 +299,16 @@ namespace StarfaceOutlookSync.Services if (match != null) { // Existiert schon -> verknuepfen und updaten - if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook)) + var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook); + if (updated != null) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = oc.OutlookEntryId, StarfaceId = match.StarfaceId, - LastSyncHash = oc.GetHash() + LastOutlookHash = oc.GetHash(), + LastStarfaceHash = updated.GetHash() }); processedStarfaceIds.Add(match.StarfaceId); unmappedStarface.Remove(match); @@ -333,7 +328,8 @@ namespace StarfaceOutlookSync.Services ProfileId = profile.Id, OutlookEntryId = oc.OutlookEntryId, StarfaceId = created.StarfaceId, - LastSyncHash = oc.GetHash() + LastOutlookHash = oc.GetHash(), + LastStarfaceHash = created.GetHash() }); result.Created++; Log($" Erstellt (OL->SF): {oc.DisplayName}"); @@ -379,14 +375,16 @@ namespace StarfaceOutlookSync.Services if (match != null) { // Existiert schon -> verknuepfen und updaten - if (_outlookService.UpdateContact(match.OutlookEntryId, sc)) + var updated = _outlookService.UpdateContact(match.OutlookEntryId, sc); + if (updated != null) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = match.OutlookEntryId, StarfaceId = sc.StarfaceId, - LastSyncHash = sc.GetHash() + LastStarfaceHash = sc.GetHash(), + LastOutlookHash = updated.GetHash() }); processedOutlookIds.Add(match.OutlookEntryId); unmappedOutlook.Remove(match); @@ -405,7 +403,8 @@ namespace StarfaceOutlookSync.Services ProfileId = profile.Id, OutlookEntryId = created.OutlookEntryId, StarfaceId = sc.StarfaceId, - LastSyncHash = sc.GetHash() + LastStarfaceHash = sc.GetHash(), + LastOutlookHash = created.GetHash() }); result.Created++; Log($" Erstellt (SF->OL): {sc.DisplayName}");