Compare commits

...

4 Commits

Author SHA1 Message Date
duffyduck ad323c9d0f Release v0.0.0.24 2026-06-08 09:58:26 +02:00
duffyduck 849a996b9a Fix sync duplicates and extreme slowness
Behebt Dubletten auf beiden Seiten und sehr langsame Syncs:

- Getrennte Hash-Baselines pro Seite (LastOutlookHash/LastStarfaceHash)
  statt eines gemeinsamen Hashes. Outlook und Starface stellen denselben
  Kontakt unterschiedlich dar, wodurch der gemeinsame Hash nie passte und
  bei jedem Lauf praktisch jeder Kontakt neu geschrieben wurde.
- Update-Methoden geben den frisch eingelesenen Stand zurueck, damit die
  Baseline nach dem Schreiben korrekt gesetzt wird (sauberes Konvergieren).
- Unvollstaendig geladene Starface-Liste bricht jetzt mit Fehler ab
  (inkl. Retry) statt still mit Teil-Liste weiterzuarbeiten - das liess
  Kontakte faelschlich als geloescht erscheinen und erzeugte Dubletten.
- Fehlender Starface-Kontakt (anderes Adressbuch) behaelt das Mapping,
  statt es zu verwerfen und neu anzulegen.
- Lockereres Re-Matching: gleicher E-Mail- oder voller Namens-Treffer
  reicht; umformatierte Telefonnummern blockieren ihn nicht mehr.
- Starface-Kontaktdetails werden parallel geladen (8 gleichzeitig).

Bestehende Mappings werden beim ersten Sync automatisch migriert.
CHANGELOG.md hinzugefuegt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:57:37 +02:00
duffyduck b07a3b3a87 Fix critical delete bug: don't mass-delete on EntryID change
Two fixes for delete propagation in Phase 1:

1. When Outlook contact not found by EntryID, try to re-match
   by name/email/phone before assuming it was deleted. Outlook
   can change EntryIDs on restart or profile changes, causing
   the sync to think ALL contacts were deleted.

2. When Starface contact not found in current list, DON'T delete
   from Outlook. The contact may belong to a different address
   book. Just drop the mapping and let Phase 2/3 re-link it.

These changes make delete propagation much safer and prevent
accidental mass-deletion of contacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:03:51 +02:00
duffyduck f620e96d23 Document Outlook security prompt suppression in README
Include step-by-step instructions for both standalone PCs and
domain/Terminal Server environments where GPO locks the setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:55:41 +02:00
9 changed files with 312 additions and 118 deletions
+65
View File
@@ -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.
+26
View File
@@ -42,6 +42,32 @@ Windows-Anwendung zur bidirektionalen Synchronisation von Kontakten zwischen Mic
5. Starface-Adressbuch und Outlook-Kontaktordner waehlen
6. Speichern und "Jetzt synchronisieren"
### Outlook-Sicherheitsabfrage unterdruecken
Beim Zugriff auf Outlook-Kontakte zeigt Outlook standardmaessig einen
Sicherheitsdialog ("Ein Programm versucht auf Ihre E-Mail-Adressinformationen
zuzugreifen"). Dieser kann in den Einstellungen der App deaktiviert werden:
1. In der App auf "Einstellungen" klicken
2. "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern
**Auf Domaenen-PCs / Terminal Servern:**
Die Outlook-Sicherheitseinstellungen werden dort per Gruppenrichtlinie (GPO)
gesteuert und sind im Trust Center ausgegraut. In diesem Fall muss die App
**einmalig als Administrator** gestartet werden, damit die Registry-Keys
unter HKLM geschrieben werden koennen:
1. Rechtsklick auf die App -> "Als Administrator ausfuehren"
2. Einstellungen -> "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern und App schliessen
4. Outlook neu starten
5. App kann danach wieder normal (ohne Admin) gestartet werden
Die Einstellung bleibt dauerhaft bestehen und gilt fuer alle Benutzer
auf dem Rechner.
### Deinstallation
Ueber Windows Einstellungen -> Apps oder die Systemsteuerung.
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.23"
#define MyAppVersion "0.0.0.24"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"
@@ -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; } = "";
}
@@ -350,7 +350,13 @@ namespace StarfaceOutlookSync.Services
}
}
public bool UpdateContact(string entryId, UnifiedContact contact)
/// <summary>
/// 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.
/// </summary>
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;
}
}
@@ -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;
}
/// <summary>
/// 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).
/// </summary>
private async Task<JObject> 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<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
{
var sfContact = MapToStarface(contact);
@@ -300,7 +348,13 @@ namespace StarfaceOutlookSync.Services
return MapFromStarface(created);
}
public async Task<bool> UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book)
/// <summary>
/// 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.
/// </summary>
public async Task<UnifiedContact> 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;
}
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.</summary>
public async Task<UnifiedContact> 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<bool> DeleteContactAsync(string contactId)
+89 -85
View File
@@ -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;
/// <summary>
/// Prueft ob zwei Felder kompatibel sind.
/// Beide leer = kompatibel. Beide gleich = kompatibel.
/// Eins leer, eins gefuellt = NICHT kompatibel (verschiedene Kontakte).
/// </summary>
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)
@@ -183,7 +149,21 @@ namespace StarfaceOutlookSync.Services
if (oc == null && sc != null)
{
// In Outlook geloescht -> in Starface auch loeschen
// Outlook-Kontakt nicht gefunden.
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
var reMatch = FindMatch(sc, outlookContacts.Where(c =>
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
if (reMatch != null)
{
// Kontakt existiert noch in Outlook, nur EntryID geaendert
Log($" EntryID geaendert, verknuepfe neu: {sc.DisplayName}");
mapping.OutlookEntryId = reMatch.OutlookEntryId;
processedOutlookIds.Add(reMatch.OutlookEntryId);
newMappings.Add(mapping);
continue;
}
// Wirklich geloescht -> in Starface auch loeschen
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)
{
if (await starface.DeleteContactAsync(mapping.StarfaceId))
@@ -194,7 +174,6 @@ namespace StarfaceOutlookSync.Services
}
else
{
// Richtung erlaubt kein Loeschen -> Mapping behalten
newMappings.Add(mapping);
}
continue;
@@ -202,37 +181,50 @@ namespace StarfaceOutlookSync.Services
if (oc != null && sc == null)
{
// In Starface geloescht -> in Outlook auch loeschen
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)
{
if (_outlookService.DeleteContact(mapping.OutlookEntryId))
{
result.Updated++;
Log($" Geloescht (SF->OL): {oc.DisplayName}");
}
}
else
{
// Richtung erlaubt kein Loeschen -> Mapping behalten
newMappings.Add(mapping);
}
// 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}");
}
@@ -240,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}");
}
@@ -301,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);
@@ -328,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}");
@@ -374,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);
@@ -400,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}");
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.0.23</Version>
<AssemblyVersion>0.0.0.23</AssemblyVersion>
<FileVersion>0.0.0.23</FileVersion>
<Version>0.0.0.24</Version>
<AssemblyVersion>0.0.0.24</AssemblyVersion>
<FileVersion>0.0.0.24</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label
{
Text = "Version 0.0.0.23",
Text = "Version 0.0.0.24",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray