Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba0b96f16a | ||
|
|
e7a87fbdf1 | ||
|
|
0076d84062 | ||
|
|
6cc3103040 | ||
|
|
844094d5c5 | ||
|
|
da20d341d7 | ||
|
|
ca05af75e3 | ||
|
|
9767299edd | ||
|
|
6ecd5d7fa3 |
@@ -7,8 +7,38 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Hinzugefuegt
|
||||||
|
|
||||||
|
- **Dubletten zusammenfuehren (Button "Dubletten").** Fuehrt doppelte Kontakte
|
||||||
|
fuer das ausgewaehlte Profil zusammen - getrennt je Seite (Telefonanlage und
|
||||||
|
Outlook). Zuerst wird analysiert und eine Vorschau (optional mit Detailliste)
|
||||||
|
gezeigt; nach Bestaetigung bleibt je Gruppe ein "Gewinner", dessen leere
|
||||||
|
Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen
|
||||||
|
werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste
|
||||||
|
bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben
|
||||||
|
Sperre wie ein Sync (kein gleichzeitiger Lauf - wartet bei laufendem Sync bis
|
||||||
|
zu 60s statt abzubrechen), nutzt dieselbe Kontakt-Erkennung wie der Sync und
|
||||||
|
wird vollstaendig protokolliert. Outlook- und Anlagen-Seite werden unabhaengig
|
||||||
|
verarbeitet (eine streikende Anlage verhindert nicht die Outlook-Bereinigung);
|
||||||
|
fehlgeschlagene Loeschungen werden gemeldet.
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
|
- **Oberflaeche friert waehrend des Syncs nicht mehr ein.** Beim Tray-/Auto-Sync
|
||||||
|
lief die Sync-Engine auf dem UI-Thread, wodurch Fenster und Tray-Kontextmenue
|
||||||
|
blockierten (Menuepunkte reagierten nicht). Die Engine laeuft jetzt im
|
||||||
|
Hintergrund (`Task.Run`), die Tray-Meldungen werden thread-sicher auf den
|
||||||
|
UI-Thread marshalled.
|
||||||
|
- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
|
||||||
|
Eintraege wurden bei jedem Sync neu angelegt statt verknuepft, weil der
|
||||||
|
Nummern-Abgleich feldgenau (geschaeftlich↔geschaeftlich) und formatabhaengig
|
||||||
|
war. Jetzt werden Rufnummern feldUEBERGREIFEND und formatunabhaengig
|
||||||
|
verglichen (`+49`/`0049` == `0`), sodass diese Eintraege verknuepft werden.
|
||||||
|
Der "Nummer + Firma"-Abgleich greift bewusst nur bei Eintraegen OHNE
|
||||||
|
Personennamen, damit keine zwei Kollegen mit gleicher Zentrale-Nummer
|
||||||
|
verschmolzen werden. (Bereits entstandene Dubletten muessen einmalig manuell
|
||||||
|
bereinigt werden.)
|
||||||
|
|
||||||
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
|
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
|
||||||
Ursachen wurden beseitigt:
|
Ursachen wurden beseitigt:
|
||||||
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
|
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
||||||
|
|
||||||
#define MyAppName "Starface Outlook Sync"
|
#define MyAppName "Starface Outlook Sync"
|
||||||
#define MyAppVersion "0.0.0.30"
|
#define MyAppVersion "0.0.3.3"
|
||||||
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
|
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
|
||||||
#define MyAppURL "https://www.hacker-net.de"
|
#define MyAppURL "https://www.hacker-net.de"
|
||||||
#define MyAppExeName "StarfaceOutlookSync.exe"
|
#define MyAppExeName "StarfaceOutlookSync.exe"
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace StarfaceOutlookSync.Models
|
namespace StarfaceOutlookSync.Models
|
||||||
{
|
{
|
||||||
|
/// <summary>Welche Telefonanlage auf der anderen Seite steht.</summary>
|
||||||
|
public enum PhoneSystem
|
||||||
|
{
|
||||||
|
Starface,
|
||||||
|
Fonaria
|
||||||
|
}
|
||||||
|
|
||||||
public enum SyncDirection
|
public enum SyncDirection
|
||||||
{
|
{
|
||||||
Both,
|
Both,
|
||||||
@@ -7,8 +16,12 @@ namespace StarfaceOutlookSync.Models
|
|||||||
StarfaceToOutlook
|
StarfaceToOutlook
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StarfaceConnection
|
public class SystemConnection
|
||||||
{
|
{
|
||||||
|
// Voreinstellung Starface, damit Profile aus der Zeit vor Fonaria
|
||||||
|
// unveraendert weiterlaufen: Was im JSON fehlt, ist eine Starface.
|
||||||
|
public PhoneSystem System { get; set; } = PhoneSystem.Starface;
|
||||||
|
|
||||||
public string Host { get; set; } = "";
|
public string Host { get; set; } = "";
|
||||||
public int Port { get; set; } = 443;
|
public int Port { get; set; } = 443;
|
||||||
public bool UseSsl { get; set; } = true;
|
public bool UseSsl { get; set; } = true;
|
||||||
@@ -16,7 +29,16 @@ namespace StarfaceOutlookSync.Models
|
|||||||
public string Password { get; set; } = "";
|
public string Password { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StarfaceAddressBook
|
/// <summary>
|
||||||
|
/// Ein Adressbuch auf der Gegenseite.
|
||||||
|
///
|
||||||
|
/// Die Felder heissen noch nach der Starface, weil dort ihre Bedeutung
|
||||||
|
/// herkommt: <c>Type</c> unterscheidet zentrales, persoenliches und
|
||||||
|
/// Tag-Adressbuch, <c>TagId</c> traegt die Kennung. Bei Fonaria steht in
|
||||||
|
/// <c>TagId</c> schlicht die Nummer des Adressbuchs und in <c>Type</c>
|
||||||
|
/// dessen Art (private, shared, system).
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAddressBook
|
||||||
{
|
{
|
||||||
public string Type { get; set; } = "central"; // central, user, tag
|
public string Type { get; set; } = "central"; // central, user, tag
|
||||||
public string UserId { get; set; } = "";
|
public string UserId { get; set; } = "";
|
||||||
@@ -30,8 +52,13 @@ namespace StarfaceOutlookSync.Models
|
|||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public StarfaceConnection StarfaceConnection { get; set; } = new StarfaceConnection();
|
// Die JSON-Namen bleiben, wie sie waren — sonst verloeren alle
|
||||||
public StarfaceAddressBook StarfaceAddressBook { get; set; } = new StarfaceAddressBook();
|
// bestehenden Profile beim ersten Start ihre Verbindungsdaten.
|
||||||
|
[JsonProperty("StarfaceConnection")]
|
||||||
|
public SystemConnection Connection { get; set; } = new SystemConnection();
|
||||||
|
|
||||||
|
[JsonProperty("StarfaceAddressBook")]
|
||||||
|
public RemoteAddressBook AddressBook { get; set; } = new RemoteAddressBook();
|
||||||
public string OutlookFolderPath { get; set; } = "";
|
public string OutlookFolderPath { get; set; } = "";
|
||||||
public string OutlookFolderName { get; set; } = "Kontakte";
|
public string OutlookFolderName { get; set; } = "Kontakte";
|
||||||
public SyncDirection SyncDirection { get; set; } = SyncDirection.Both;
|
public SyncDirection SyncDirection { get; set; } = SyncDirection.Both;
|
||||||
@@ -44,6 +71,10 @@ namespace StarfaceOutlookSync.Models
|
|||||||
{
|
{
|
||||||
public string ProfileId { get; set; } = "";
|
public string ProfileId { get; set; } = "";
|
||||||
public string OutlookEntryId { get; set; } = "";
|
public string OutlookEntryId { get; set; } = "";
|
||||||
|
|
||||||
|
// Die Kennung auf der Gegenseite — bei Fonaria die Kontakt-ID. Der
|
||||||
|
// Name bleibt: Er steht in jeder bereits gespeicherten Zuordnung, und
|
||||||
|
// ein Umbenennen wuerde sie alle entwerten.
|
||||||
public string StarfaceId { get; set; } = "";
|
public string StarfaceId { get; set; } = "";
|
||||||
|
|
||||||
// Getrennte Baselines pro Seite. Outlook und Starface stellen denselben
|
// Getrennte Baselines pro Seite. Outlook und Starface stellen denselben
|
||||||
|
|||||||
@@ -2,6 +2,22 @@ namespace StarfaceOutlookSync.Models
|
|||||||
{
|
{
|
||||||
public class UnifiedContact
|
public class UnifiedContact
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Welche vCard-Eigenschaften dieses Modell abbildet.
|
||||||
|
///
|
||||||
|
/// Fonaria fuehrt den Kontakt im vollen vCard-Umfang — Kategorien,
|
||||||
|
/// Jahrestag, Spitzname, Bild, zweite Anschrift. Hier steht davon nur
|
||||||
|
/// ein Ausschnitt, weil die Starface nicht mehr kann. Beim Schreiben
|
||||||
|
/// nach Fonaria wird diese Liste mitgeschickt: Sie sagt der Anlage,
|
||||||
|
/// wofuer wir zustaendig sind. Alles andere bleibt dort unangetastet,
|
||||||
|
/// statt geloescht zu werden, nur weil wir es nicht kennen.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string[] VcardFelder =
|
||||||
|
{
|
||||||
|
"FN", "N", "ORG", "TITLE", "TEL", "EMAIL", "ADR", "URL", "NOTE", "BDAY"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
public string OutlookEntryId { get; set; } = "";
|
public string OutlookEntryId { get; set; } = "";
|
||||||
public string StarfaceId { get; set; } = "";
|
public string StarfaceId { get; set; } = "";
|
||||||
public string FirstName { get; set; } = "";
|
public string FirstName { get; set; } = "";
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Erkennt, ob zwei Kontakte dieselbe Person/Firma sind. Wird sowohl vom
|
||||||
|
/// Sync (Wiederzuordnung, Duplikat-Vermeidung) als auch von der
|
||||||
|
/// Dubletten-Zusammenfuehrung genutzt - beide MUESSEN dieselbe Logik
|
||||||
|
/// verwenden, sonst laufen sie gegeneinander.
|
||||||
|
/// </summary>
|
||||||
|
public static class ContactMatcher
|
||||||
|
{
|
||||||
|
public static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
|
||||||
|
{
|
||||||
|
if (candidates == null || candidates.Count == 0) return null;
|
||||||
|
foreach (var c in candidates)
|
||||||
|
if (IsMatch(contact, c))
|
||||||
|
return c;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsMatch(UnifiedContact a, UnifiedContact b)
|
||||||
|
{
|
||||||
|
bool hasName = (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName))
|
||||||
|
&& (!string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName));
|
||||||
|
|
||||||
|
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);
|
||||||
|
// Telefon feldUEBERGREIFEND und formatunabhaengig (+49 == 0).
|
||||||
|
bool phoneMatch = SharedPhone(a, b);
|
||||||
|
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
|
||||||
|
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch;
|
||||||
|
|
||||||
|
if (emailMatch) return true;
|
||||||
|
if (nameMatch && !emailContradiction) return true;
|
||||||
|
|
||||||
|
// Firma-/Service-Eintraege OHNE Personennamen: gleiche Firma + gemeinsame
|
||||||
|
// Nummer. Bewusst nur ohne Namen, damit nicht zwei Kollegen mit gleicher
|
||||||
|
// Zentrale-Nummer + Firma faelschlich verschmolzen werden.
|
||||||
|
bool aHasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
|
||||||
|
bool bHasName = !string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName);
|
||||||
|
if (phoneMatch && companyMatch && !aHasName && !bHasName && !emailContradiction) return true;
|
||||||
|
|
||||||
|
// Reine Nummern-Eintraege: eine gemeinsame Rufnummer identifiziert den Eintrag.
|
||||||
|
if (phoneMatch && IsBareNumberEntry(a) && IsBareNumberEntry(b)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBareNumberEntry(UnifiedContact c) =>
|
||||||
|
string.IsNullOrEmpty(c.FirstName) && string.IsNullOrEmpty(c.LastName)
|
||||||
|
&& string.IsNullOrEmpty(c.Company) && string.IsNullOrEmpty(c.Email);
|
||||||
|
|
||||||
|
/// <summary>True, wenn a und b mindestens eine (normalisierte) Rufnummer gemeinsam haben.</summary>
|
||||||
|
public static bool SharedPhone(UnifiedContact a, UnifiedContact b)
|
||||||
|
{
|
||||||
|
var pa = PhoneSet(a);
|
||||||
|
if (pa.Count == 0) return false;
|
||||||
|
var pb = PhoneSet(b);
|
||||||
|
return pb.Count > 0 && pa.Overlaps(pb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> PhoneSet(UnifiedContact c)
|
||||||
|
{
|
||||||
|
var set = new HashSet<string>();
|
||||||
|
foreach (var p in new[] { c.PhoneWork, c.PhoneMobile, c.PhoneHome, c.Fax })
|
||||||
|
{
|
||||||
|
var n = NormalizePhone(p);
|
||||||
|
if (n.Length >= 5) set.Add(n); // zu kurze (z.B. reine Durchwahlen) ignorieren
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizePhone(string phone)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(phone)) return "";
|
||||||
|
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||||
|
if (s.StartsWith("+49")) s = "0" + s.Substring(3);
|
||||||
|
else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
|
||||||
|
else if (s.StartsWith("+")) s = s.Substring(1);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,34 @@ namespace StarfaceOutlookSync.Services
|
|||||||
new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuellt LEERE Felder von target aus den sources auf (erste nicht-leere
|
||||||
|
/// Quelle gewinnt). Vorhandene Werte von target bleiben unveraendert -
|
||||||
|
/// kein Datenverlust. Gibt true zurueck, wenn sich etwas geaendert hat.
|
||||||
|
/// </summary>
|
||||||
|
public static bool FillEmptyInto(UnifiedContact target, IEnumerable<UnifiedContact> sources)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
var list = sources?.ToList() ?? new List<UnifiedContact>();
|
||||||
|
foreach (var f in Fields)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(f.Get(target))) continue;
|
||||||
|
foreach (var s in list)
|
||||||
|
{
|
||||||
|
var v = f.Get(s);
|
||||||
|
if (!string.IsNullOrEmpty(v)) { f.Set(target, v); changed = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Anzahl gefuellter Inhaltsfelder - fuer die Wahl des "Gewinners".</summary>
|
||||||
|
public static int FilledFieldCount(UnifiedContact c)
|
||||||
|
{
|
||||||
|
if (c == null) return 0;
|
||||||
|
return Fields.Count(f => !string.IsNullOrEmpty(f.Get(c)));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
|
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
|
||||||
public static string GetValue(UnifiedContact c, string key)
|
public static string GetValue(UnifiedContact c, string key)
|
||||||
{
|
{
|
||||||
@@ -140,7 +168,11 @@ namespace StarfaceOutlookSync.Services
|
|||||||
private static string NormalizePhone(string phone)
|
private static string NormalizePhone(string phone)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(phone)) return "";
|
if (string.IsNullOrEmpty(phone)) return "";
|
||||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||||
|
if (s.StartsWith("+49")) s = "0" + s.Substring(3);
|
||||||
|
else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
|
||||||
|
else if (s.StartsWith("+")) s = s.Substring(1);
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt Dubletten innerhalb EINES Profils zusammen - getrennt je Seite
|
||||||
|
/// (Telefonanlage und Outlook). Regel: ein "Gewinner" bleibt, dessen leere
|
||||||
|
/// Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die
|
||||||
|
/// uebrigen werden geloescht. Danach werden verwaiste Zuordnungen entfernt,
|
||||||
|
/// damit der naechste (bidirektionale) Sync die Gewinner sauber 1:1 verbindet.
|
||||||
|
/// Nutzt dieselbe Erkennung wie der Sync (ContactMatcher).
|
||||||
|
/// </summary>
|
||||||
|
public class DedupeService
|
||||||
|
{
|
||||||
|
private readonly ProfileManager _profileManager = new ProfileManager();
|
||||||
|
private readonly OutlookContactsService _outlookService = new OutlookContactsService();
|
||||||
|
|
||||||
|
public event Action<string> OnProgress;
|
||||||
|
private void Log(string m) => OnProgress?.Invoke(m);
|
||||||
|
|
||||||
|
public class DupGroup
|
||||||
|
{
|
||||||
|
public UnifiedContact Survivor;
|
||||||
|
public List<UnifiedContact> Duplicates = new List<UnifiedContact>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DedupePlan
|
||||||
|
{
|
||||||
|
public List<DupGroup> RemoteGroups = new List<DupGroup>(); // Telefonanlage
|
||||||
|
public List<DupGroup> OutlookGroups = new List<DupGroup>();
|
||||||
|
public int RemoteDeletions => RemoteGroups.Sum(g => g.Duplicates.Count);
|
||||||
|
public int OutlookDeletions => OutlookGroups.Sum(g => g.Duplicates.Count);
|
||||||
|
public bool HasWork => RemoteGroups.Count > 0 || OutlookGroups.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DedupeReport
|
||||||
|
{
|
||||||
|
public int RemoteMerged, RemoteDeleted, OutlookMerged, OutlookDeleted, Errors;
|
||||||
|
public List<string> Messages = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur analysieren (lesend), fuer die Vorschau.</summary>
|
||||||
|
public async Task<DedupePlan> AnalyzeAsync(SyncProfile profile)
|
||||||
|
{
|
||||||
|
var plan = new DedupePlan();
|
||||||
|
|
||||||
|
var mappings = _profileManager.GetMappings(profile.Id);
|
||||||
|
var mappedStarface = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.StarfaceId)).Select(m => m.StarfaceId));
|
||||||
|
var mappedOutlook = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.OutlookEntryId)).Select(m => m.OutlookEntryId));
|
||||||
|
|
||||||
|
Log("Lade Outlook-Kontakte...");
|
||||||
|
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
|
||||||
|
|
||||||
|
Log($"Verbinde mit {profile.Connection.System}...");
|
||||||
|
using (var backend = ContactBackendFactory.Create(profile.Connection))
|
||||||
|
{
|
||||||
|
backend.OnDebug += Log;
|
||||||
|
if (!await backend.LoginAsync())
|
||||||
|
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
|
||||||
|
|
||||||
|
Log("Lade Kontakte der Telefonanlage...");
|
||||||
|
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
|
||||||
|
await backend.LogoutAsync();
|
||||||
|
|
||||||
|
plan.RemoteGroups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
|
||||||
|
plan.OutlookGroups = GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Analyse fertig: Anlage {plan.RemoteGroups.Count} Gruppe(n)/{plan.RemoteDeletions} zu loeschen, " +
|
||||||
|
$"Outlook {plan.OutlookGroups.Count} Gruppe(n)/{plan.OutlookDeletions} zu loeschen.");
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt die Zusammenfuehrung wirklich aus. Laedt frisch (unter Sperre
|
||||||
|
/// aufrufen!), gruppiert und wendet an. Gibt einen Bericht zurueck.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DedupeReport> ExecuteAsync(SyncProfile profile)
|
||||||
|
{
|
||||||
|
var report = new DedupeReport();
|
||||||
|
|
||||||
|
var mappings = _profileManager.GetMappings(profile.Id);
|
||||||
|
var mappedStarface = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.StarfaceId)).Select(m => m.StarfaceId));
|
||||||
|
var mappedOutlook = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.OutlookEntryId)).Select(m => m.OutlookEntryId));
|
||||||
|
|
||||||
|
var deletedStarfaceIds = new HashSet<string>();
|
||||||
|
var deletedOutlookIds = new HashSet<string>();
|
||||||
|
|
||||||
|
// --- Outlook (lokal, unabhaengig - laeuft auch wenn die Anlage streikt) ---
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
|
||||||
|
var groups = GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId));
|
||||||
|
Log($"Outlook: {groups.Count} Gruppe(n) mit Dubletten.");
|
||||||
|
foreach (var g in groups)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
|
||||||
|
if (changed && _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) == null)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var dup in g.Duplicates)
|
||||||
|
{
|
||||||
|
if (_outlookService.DeleteContact(dup.OutlookEntryId))
|
||||||
|
{
|
||||||
|
deletedOutlookIds.Add(dup.OutlookEntryId);
|
||||||
|
report.OutlookDeleted++;
|
||||||
|
Log($" Outlook: Dublette geloescht -> '{g.Survivor.DisplayName}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Outlook: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.OutlookMerged++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add("Outlook gesamt: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Telefonanlage (unabhaengig) ---
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var backend = ContactBackendFactory.Create(profile.Connection))
|
||||||
|
{
|
||||||
|
backend.OnDebug += Log;
|
||||||
|
if (!await backend.LoginAsync())
|
||||||
|
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
|
||||||
|
|
||||||
|
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
|
||||||
|
var groups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
|
||||||
|
Log($"Telefonanlage: {groups.Count} Gruppe(n) mit Dubletten.");
|
||||||
|
foreach (var g in groups)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
|
||||||
|
if (changed && await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) == null)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var dup in g.Duplicates)
|
||||||
|
{
|
||||||
|
if (await backend.DeleteContactAsync(dup.StarfaceId))
|
||||||
|
{
|
||||||
|
deletedStarfaceIds.Add(dup.StarfaceId);
|
||||||
|
report.RemoteDeleted++;
|
||||||
|
Log($" Anlage: Dublette geloescht -> '{g.Survivor.DisplayName}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Anlage: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.RemoteMerged++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await backend.LogoutAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add("Telefonanlage gesamt: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen.
|
||||||
|
// Der naechste Sync verbindet die Gewinner sauber neu.
|
||||||
|
if (deletedStarfaceIds.Count > 0 || deletedOutlookIds.Count > 0)
|
||||||
|
{
|
||||||
|
var kept = mappings
|
||||||
|
.Where(m => !deletedStarfaceIds.Contains(m.StarfaceId) && !deletedOutlookIds.Contains(m.OutlookEntryId))
|
||||||
|
.ToList();
|
||||||
|
_profileManager.SaveMappings(profile.Id, kept);
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Gruppierung ----
|
||||||
|
|
||||||
|
private static List<DupGroup> GroupDuplicates(List<UnifiedContact> contacts, Func<UnifiedContact, bool> isMapped)
|
||||||
|
{
|
||||||
|
var groups = new List<DupGroup>();
|
||||||
|
if (contacts == null) return groups;
|
||||||
|
var used = new bool[contacts.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < contacts.Count; i++)
|
||||||
|
{
|
||||||
|
if (used[i]) continue;
|
||||||
|
var members = new List<UnifiedContact> { contacts[i] };
|
||||||
|
used[i] = true;
|
||||||
|
|
||||||
|
for (int j = i + 1; j < contacts.Count; j++)
|
||||||
|
{
|
||||||
|
if (used[j]) continue;
|
||||||
|
if (members.Any(m => ContactMatcher.IsMatch(m, contacts[j])))
|
||||||
|
{
|
||||||
|
members.Add(contacts[j]);
|
||||||
|
used[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (members.Count > 1)
|
||||||
|
{
|
||||||
|
var survivor = ChooseSurvivor(members, isMapped);
|
||||||
|
groups.Add(new DupGroup
|
||||||
|
{
|
||||||
|
Survivor = survivor,
|
||||||
|
Duplicates = members.Where(m => !ReferenceEquals(m, survivor)).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnifiedContact ChooseSurvivor(List<UnifiedContact> members, Func<UnifiedContact, bool> isMapped)
|
||||||
|
{
|
||||||
|
// 1) einen bereits zugeordneten bevorzugen (Kontinuitaet),
|
||||||
|
// 2) sonst den mit den meisten gefuellten Feldern.
|
||||||
|
return members
|
||||||
|
.OrderByDescending(m => isMapped(m) ? 1 : 0)
|
||||||
|
.ThenByDescending(m => ContactMerger.FilledFieldCount(m))
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Die Gegenseite Fonaria.
|
||||||
|
///
|
||||||
|
/// Anders als die Starface haelt Fonaria jeden Kontakt als vollstaendige
|
||||||
|
/// vCard vor — mit Bild, Kategorien, Jahrestag, mehreren Anschriften. Das
|
||||||
|
/// Sync-Modell dieser Anwendung bildet davon nur einen Ausschnitt ab.
|
||||||
|
/// Deshalb schickt jeder Schreibvorgang <c>verwaltet</c> mit: die Liste der
|
||||||
|
/// Eigenschaften, fuer die wir uns zustaendig erklaeren. Die Anlage
|
||||||
|
/// schreibt genau diese in die vorhandene Karte und laesst den Rest stehen.
|
||||||
|
///
|
||||||
|
/// Ohne das waere der erste Abgleich verheerend: Ein aus Outlook
|
||||||
|
/// gespiegelter Kontakt haette hinterher kein Bild und keine Kategorien
|
||||||
|
/// mehr — nicht weil jemand sie geloescht haette, sondern weil wir sie
|
||||||
|
/// nicht kannten.
|
||||||
|
/// </summary>
|
||||||
|
public class FonariaApiClient : IContactBackend
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly SystemConnection _connection;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private string _token;
|
||||||
|
|
||||||
|
public event Action<string> OnDebug;
|
||||||
|
|
||||||
|
public string SystemName => "Fonaria";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> VerwalteteFelder => UnifiedContact.VcardFelder;
|
||||||
|
|
||||||
|
public FonariaApiClient(SystemConnection connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
// Wie bei der Starface: Anlagen im eigenen Netz tragen oft ein
|
||||||
|
// selbst ausgestelltes Zertifikat.
|
||||||
|
handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
|
||||||
|
|
||||||
|
_http = new HttpClient(handler);
|
||||||
|
_http.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
var protocol = connection.UseSsl ? "https" : "http";
|
||||||
|
var portPart = (connection.UseSsl && connection.Port == 443) ||
|
||||||
|
(!connection.UseSsl && connection.Port == 80)
|
||||||
|
? "" : $":{connection.Port}";
|
||||||
|
_baseUrl = $"{protocol}://{connection.Host}{portPart}/api/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Anmeldung
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
public async Task<bool> LoginAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var body = new { username = _connection.LoginId, password = _connection.Password };
|
||||||
|
var content = new StringContent(
|
||||||
|
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var resp = await _http.PostAsync($"{_baseUrl}/auth/login", content);
|
||||||
|
var respBody = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Fonaria-Anmeldung fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_token = JObject.Parse(respBody)["access_token"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(_token)) return false;
|
||||||
|
|
||||||
|
_http.DefaultRequestHeaders.Authorization =
|
||||||
|
new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Fonaria-Anmeldung fehlgeschlagen: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LogoutAsync()
|
||||||
|
{
|
||||||
|
// Fonaria-Token laufen von selbst ab; es gibt nichts abzumelden.
|
||||||
|
_token = null;
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Lesen
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
public async Task<List<RemoteAddressBook>> GetAddressBooksAsync()
|
||||||
|
{
|
||||||
|
var buecher = new List<RemoteAddressBook>();
|
||||||
|
var resp = await _http.GetAsync($"{_baseUrl}/addressbooks");
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Adressbuecher nicht abrufbar: {(int)resp.StatusCode}");
|
||||||
|
return buecher;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var eintrag in JArray.Parse(await resp.Content.ReadAsStringAsync()))
|
||||||
|
{
|
||||||
|
var art = eintrag["kind"]?.ToString() ?? "shared";
|
||||||
|
// Das Benutzerverzeichnis pflegt die Anlage selbst und weist
|
||||||
|
// jeden Schreibversuch ab. Als Sync-Ziel waere es eine Falle.
|
||||||
|
if (art == "system") continue;
|
||||||
|
|
||||||
|
buecher.Add(new RemoteAddressBook
|
||||||
|
{
|
||||||
|
Type = art,
|
||||||
|
TagId = eintrag["id"]?.ToString() ?? "",
|
||||||
|
Name = eintrag["name"]?.ToString() ?? "",
|
||||||
|
UserId = eintrag["owner_id"]?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return buecher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book)
|
||||||
|
{
|
||||||
|
// Bewusst mit full=1: Die Kurzform der Liste traegt nur Name, Firma
|
||||||
|
// und Nummern. Wer damit abgleicht, haelt jeden Kontakt fuer
|
||||||
|
// veraendert und schreibt bei jedem Durchlauf alles neu.
|
||||||
|
var url = $"{_baseUrl}/addressbooks/{book.TagId}/contacts?full=1&limit=5000";
|
||||||
|
|
||||||
|
HttpResponseMessage resp = null;
|
||||||
|
for (var versuch = 0; versuch < 3; versuch++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resp = await _http.GetAsync(url);
|
||||||
|
if (resp.IsSuccessStatusCode) break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Kontaktabruf gescheitert (Versuch {versuch + 1}): {ex.Message}");
|
||||||
|
}
|
||||||
|
await Task.Delay(250 * (versuch + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mit einer halben Liste weiterzuarbeiten ist gefaehrlicher als
|
||||||
|
// abzubrechen: Fehlende Kontakte gaelten als geloescht.
|
||||||
|
if (resp == null || !resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception(
|
||||||
|
"Die Kontakte aus Fonaria konnten nicht geladen werden. " +
|
||||||
|
"Synchronisation abgebrochen, um Dubletten und Loeschungen zu vermeiden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var kontakte = new List<UnifiedContact>();
|
||||||
|
foreach (var eintrag in JArray.Parse(await resp.Content.ReadAsStringAsync()))
|
||||||
|
kontakte.Add(AusFonaria(eintrag));
|
||||||
|
|
||||||
|
OnDebug?.Invoke($"{kontakte.Count} Kontakte aus Fonaria-Adressbuch {book.Name} geladen");
|
||||||
|
return kontakte;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resp = await _http.GetAsync(
|
||||||
|
$"{_baseUrl}/addressbooks/{book.TagId}/contacts/{contactId}");
|
||||||
|
if (resp.StatusCode == HttpStatusCode.NotFound) return null;
|
||||||
|
if (!resp.IsSuccessStatusCode) return null;
|
||||||
|
return AusFonaria(JObject.Parse(await resp.Content.ReadAsStringAsync()));
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Schreiben
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book)
|
||||||
|
{
|
||||||
|
var body = NachFonaria(contact, null).ToString();
|
||||||
|
OnDebug?.Invoke($"POST /addressbooks/{book.TagId}/contacts");
|
||||||
|
|
||||||
|
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _http.PostAsync($"{_baseUrl}/addressbooks/{book.TagId}/contacts", content);
|
||||||
|
var respBody = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Anlegen fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return AusFonaria(JObject.Parse(respBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnifiedContact> UpdateContactAsync(
|
||||||
|
string contactId, UnifiedContact contact, RemoteAddressBook book)
|
||||||
|
{
|
||||||
|
// Den bekannten Stand mitgeben, damit nichts verlorengeht, was
|
||||||
|
// innerhalb einer verwalteten Eigenschaft liegt, aber im
|
||||||
|
// Sync-Modell nicht vorkommt.
|
||||||
|
JToken roh;
|
||||||
|
if (!_rohdaten.TryGetValue(contactId, out roh))
|
||||||
|
{
|
||||||
|
var vorher = await GetContactAsync(contactId, book);
|
||||||
|
_rohdaten.TryGetValue(contactId, out roh);
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = NachFonaria(contact, roh).ToString();
|
||||||
|
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _http.PutAsync(
|
||||||
|
$"{_baseUrl}/addressbooks/{book.TagId}/contacts/{contactId}", content);
|
||||||
|
var respBody = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
OnDebug?.Invoke($"Aendern von {contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Der Stand *nach* dem Schreiben ist massgeblich fuer die Baseline —
|
||||||
|
// sonst gilt der Kontakt beim naechsten Lauf erneut als geaendert.
|
||||||
|
return AusFonaria(JObject.Parse(respBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteContactAsync(string contactId)
|
||||||
|
{
|
||||||
|
// Ohne Adressbuch im Pfad geht es bei Fonaria nicht. Die Engine
|
||||||
|
// ruft nur mit Kontakten des Profil-Buches, deshalb reicht das
|
||||||
|
// zuletzt benutzte.
|
||||||
|
if (_letztesBuch == null) return false;
|
||||||
|
var resp = await _http.DeleteAsync(
|
||||||
|
$"{_baseUrl}/addressbooks/{_letztesBuch}/contacts/{contactId}");
|
||||||
|
return resp.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _letztesBuch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Die Rohdaten der zuletzt gelesenen Kontakte, nach Kennung.
|
||||||
|
///
|
||||||
|
/// Sie werden beim Schreiben gebraucht: Zwei Dinge liegen innerhalb
|
||||||
|
/// einer Eigenschaft, die wir verwalten, gehoeren aber trotzdem nicht
|
||||||
|
/// uns — die Abteilung steckt im zweiten Teil von ORG, und neben der
|
||||||
|
/// geschaeftlichen Anschrift kann eine private stehen. Beides kennt das
|
||||||
|
/// Sync-Modell nicht. Wer es nicht zurueckschreibt, loescht es.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, JToken> _rohdaten = new Dictionary<string, JToken>();
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Umwandlung
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
private UnifiedContact AusFonaria(JToken item)
|
||||||
|
{
|
||||||
|
_letztesBuch = item["address_book_id"]?.ToString() ?? _letztesBuch;
|
||||||
|
var kennung = item["id"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(kennung)) _rohdaten[kennung] = item;
|
||||||
|
|
||||||
|
var kontakt = new UnifiedContact
|
||||||
|
{
|
||||||
|
StarfaceId = item["id"]?.ToString() ?? "",
|
||||||
|
FirstName = item["first_name"]?.ToString() ?? "",
|
||||||
|
LastName = item["last_name"]?.ToString() ?? "",
|
||||||
|
Company = item["organization"]?.ToString() ?? "",
|
||||||
|
JobTitle = item["title"]?.ToString() ?? "",
|
||||||
|
Salutation = item["prefix"]?.ToString() ?? "",
|
||||||
|
Notes = item["notes"]?.ToString() ?? "",
|
||||||
|
Birthday = item["birthday"]?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var mail in Eintraege(item["emails"]))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(kontakt.Email)) kontakt.Email = mail.Item1;
|
||||||
|
else if (string.IsNullOrEmpty(kontakt.EmailSecondary)) kontakt.EmailSecondary = mail.Item1;
|
||||||
|
}
|
||||||
|
// Die schlanke Liste fuehrt nur ein einzelnes Feld.
|
||||||
|
if (string.IsNullOrEmpty(kontakt.Email))
|
||||||
|
kontakt.Email = item["email"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
foreach (var nummer in Nummern(item["numbers"]))
|
||||||
|
{
|
||||||
|
switch (nummer.Item2)
|
||||||
|
{
|
||||||
|
case "mobil": if (string.IsNullOrEmpty(kontakt.PhoneMobile)) kontakt.PhoneMobile = nummer.Item1; break;
|
||||||
|
case "home": if (string.IsNullOrEmpty(kontakt.PhoneHome)) kontakt.PhoneHome = nummer.Item1; break;
|
||||||
|
case "fax": if (string.IsNullOrEmpty(kontakt.Fax)) kontakt.Fax = nummer.Item1; break;
|
||||||
|
default: if (string.IsNullOrEmpty(kontakt.PhoneWork)) kontakt.PhoneWork = nummer.Item1; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var webadresse = Eintraege(item["urls"]).FirstOrDefault();
|
||||||
|
if (webadresse != null) kontakt.Website = webadresse.Item1;
|
||||||
|
|
||||||
|
// Von mehreren Anschriften traegt das Modell nur eine. Die
|
||||||
|
// geschaeftliche hat Vorrang — das ist die, um die es im
|
||||||
|
// Telefonbuch einer Anlage geht.
|
||||||
|
var anschriften = item["addresses"] as JArray;
|
||||||
|
if (anschriften != null && anschriften.Count > 0)
|
||||||
|
{
|
||||||
|
var gewaehlt = anschriften.FirstOrDefault(a => a["art"]?.ToString() == "work")
|
||||||
|
?? anschriften[0];
|
||||||
|
kontakt.Street = gewaehlt["strasse"]?.ToString() ?? "";
|
||||||
|
kontakt.City = gewaehlt["ort"]?.ToString() ?? "";
|
||||||
|
kontakt.PostalCode = gewaehlt["plz"]?.ToString() ?? "";
|
||||||
|
kontakt.State = gewaehlt["region"]?.ToString() ?? "";
|
||||||
|
kontakt.Country = gewaehlt["land"]?.ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return kontakt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Tuple<string, string>> Eintraege(JToken liste)
|
||||||
|
{
|
||||||
|
var raus = new List<Tuple<string, string>>();
|
||||||
|
if (!(liste is JArray felder)) return raus;
|
||||||
|
foreach (var f in felder)
|
||||||
|
{
|
||||||
|
var wert = f["value"]?.ToString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(wert))
|
||||||
|
raus.Add(Tuple.Create(wert, f["kind"]?.ToString() ?? ""));
|
||||||
|
}
|
||||||
|
return raus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Tuple<string, string>> Nummern(JToken liste)
|
||||||
|
{
|
||||||
|
var raus = new List<Tuple<string, string>>();
|
||||||
|
if (!(liste is JArray felder)) return raus;
|
||||||
|
foreach (var f in felder)
|
||||||
|
{
|
||||||
|
// Die Kurzform der Liste nennt das Feld "number", die
|
||||||
|
// Vollansicht "value".
|
||||||
|
var wert = f["number"]?.ToString() ?? f["value"]?.ToString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(wert))
|
||||||
|
raus.Add(Tuple.Create(wert, f["kind"]?.ToString() ?? "work"));
|
||||||
|
}
|
||||||
|
return raus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JObject NachFonaria(UnifiedContact contact, JToken bekannt)
|
||||||
|
{
|
||||||
|
var nummern = new JArray();
|
||||||
|
void Nummer(string wert, string art)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(wert))
|
||||||
|
nummern.Add(new JObject { ["value"] = wert, ["kind"] = art });
|
||||||
|
}
|
||||||
|
Nummer(contact.PhoneWork, "work");
|
||||||
|
Nummer(contact.PhoneMobile, "mobil");
|
||||||
|
Nummer(contact.PhoneHome, "home");
|
||||||
|
Nummer(contact.Fax, "fax");
|
||||||
|
|
||||||
|
var mails = new JArray();
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.Email))
|
||||||
|
mails.Add(new JObject { ["value"] = contact.Email, ["kind"] = "work" });
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.EmailSecondary))
|
||||||
|
mails.Add(new JObject { ["value"] = contact.EmailSecondary, ["kind"] = "home" });
|
||||||
|
|
||||||
|
var webadressen = new JArray();
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.Website))
|
||||||
|
webadressen.Add(new JObject { ["value"] = contact.Website, ["kind"] = "work" });
|
||||||
|
|
||||||
|
// Das Sync-Modell traegt genau eine Anschrift, die geschaeftliche.
|
||||||
|
// Alle uebrigen der Anlage werden unveraendert wieder mitgeschickt.
|
||||||
|
var anschriften = new JArray();
|
||||||
|
if (!string.IsNullOrWhiteSpace(contact.Street) ||
|
||||||
|
!string.IsNullOrWhiteSpace(contact.City) ||
|
||||||
|
!string.IsNullOrWhiteSpace(contact.PostalCode))
|
||||||
|
{
|
||||||
|
anschriften.Add(new JObject
|
||||||
|
{
|
||||||
|
["art"] = "work",
|
||||||
|
["strasse"] = contact.Street ?? "",
|
||||||
|
["ort"] = contact.City ?? "",
|
||||||
|
["plz"] = contact.PostalCode ?? "",
|
||||||
|
["region"] = contact.State ?? "",
|
||||||
|
["land"] = contact.Country ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (bekannt?["addresses"] is JArray vorhandene)
|
||||||
|
{
|
||||||
|
foreach (var a in vorhandene)
|
||||||
|
if (a["art"]?.ToString() != "work")
|
||||||
|
anschriften.Add(a.DeepClone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dasselbe fuer die Abteilung: Sie steckt im zweiten Teil von ORG,
|
||||||
|
// das Sync-Modell kennt nur die Firma.
|
||||||
|
var abteilung = bekannt?["department"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
return new JObject
|
||||||
|
{
|
||||||
|
// Der Kern: Nur diese Eigenschaften fasst Fonaria an.
|
||||||
|
["verwaltet"] = new JArray(VerwalteteFelder),
|
||||||
|
|
||||||
|
["display_name"] = contact.DisplayName,
|
||||||
|
["prefix"] = contact.Salutation ?? "",
|
||||||
|
["first_name"] = contact.FirstName ?? "",
|
||||||
|
["last_name"] = contact.LastName ?? "",
|
||||||
|
["organization"] = contact.Company ?? "",
|
||||||
|
["department"] = abteilung,
|
||||||
|
["title"] = contact.JobTitle ?? "",
|
||||||
|
["numbers"] = nummern,
|
||||||
|
["emails"] = mails,
|
||||||
|
["urls"] = webadressen,
|
||||||
|
["addresses"] = anschriften,
|
||||||
|
["birthday"] = contact.Birthday ?? "",
|
||||||
|
["notes"] = string.IsNullOrEmpty(contact.Notes) ? null : contact.Notes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_http?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Die Gegenseite eines Abgleichs — eine Telefonanlage mit Adressbuechern.
|
||||||
|
///
|
||||||
|
/// Die SyncEngine kennt nur dieses Interface. Was dahinter steckt, eine
|
||||||
|
/// Starface oder eine Fonaria, entscheidet allein das Profil. Ohne diese
|
||||||
|
/// Trennung muesste die Engine fuer jede weitere Anlage angefasst werden —
|
||||||
|
/// und in ihr steckt die ganze Erfahrung mit getrennten Baselines,
|
||||||
|
/// Wiederzuordnung und Konfliktaufloesung, die man nicht zweimal haben will.
|
||||||
|
/// </summary>
|
||||||
|
public interface IContactBackend : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>Ausfuehrliche Meldungen fuer das Protokoll.</summary>
|
||||||
|
event Action<string> OnDebug;
|
||||||
|
|
||||||
|
/// <summary>Welche Anlage ist das? Nur fuer Meldungen an den Anwender.</summary>
|
||||||
|
string SystemName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Welche vCard-Eigenschaften diese Anlage ueberhaupt fuehrt.
|
||||||
|
///
|
||||||
|
/// Entscheidend beim Schreiben: Fonaria haelt den Kontakt im vollen
|
||||||
|
/// vCard-Umfang. Ein Abgleich mit einer Starface darf deshalb nicht
|
||||||
|
/// die ganze Karte ersetzen — die Starface kennt weder Kategorien noch
|
||||||
|
/// Jahrestag noch Bild und wuerde sie beim ersten Durchlauf
|
||||||
|
/// mitloeschen, nur weil sie sie nicht mitschickt. Diese Liste sagt der
|
||||||
|
/// Gegenseite, wofuer wir uns zustaendig fuehlen.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<string> VerwalteteFelder { get; }
|
||||||
|
|
||||||
|
Task<bool> LoginAsync();
|
||||||
|
Task LogoutAsync();
|
||||||
|
|
||||||
|
Task<List<RemoteAddressBook>> GetAddressBooksAsync();
|
||||||
|
Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book);
|
||||||
|
Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book);
|
||||||
|
|
||||||
|
Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book);
|
||||||
|
Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, RemoteAddressBook book);
|
||||||
|
Task<bool> DeleteContactAsync(string contactId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Waehlt die Umsetzung, die zum Profil passt.</summary>
|
||||||
|
public static class ContactBackendFactory
|
||||||
|
{
|
||||||
|
public static IContactBackend Create(SystemConnection connection)
|
||||||
|
{
|
||||||
|
switch (connection.System)
|
||||||
|
{
|
||||||
|
case PhoneSystem.Fonaria:
|
||||||
|
return new FonariaApiClient(connection);
|
||||||
|
default:
|
||||||
|
return new StarfaceApiClient(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,25 @@ using StarfaceOutlookSync.Models;
|
|||||||
|
|
||||||
namespace StarfaceOutlookSync.Services
|
namespace StarfaceOutlookSync.Services
|
||||||
{
|
{
|
||||||
public class StarfaceApiClient : IDisposable
|
public class StarfaceApiClient : IContactBackend
|
||||||
{
|
{
|
||||||
|
public string SystemName => "Starface";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Was eine Starface an Feldern kennt — mehr gibt ihr Datenmodell nicht
|
||||||
|
/// her. Beim Schreiben nach Fonaria zaehlt genau diese Liste: Alles
|
||||||
|
/// andere in der dortigen vCard bleibt unangetastet, statt hier
|
||||||
|
/// mangels Kenntnis geloescht zu werden.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> VerwalteteFelder => UnifiedContact.VcardFelder;
|
||||||
|
|
||||||
|
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
private readonly StarfaceConnection _connection;
|
private readonly SystemConnection _connection;
|
||||||
private readonly string _baseUrl;
|
private readonly string _baseUrl;
|
||||||
private string _token;
|
private string _token;
|
||||||
|
|
||||||
public StarfaceApiClient(StarfaceConnection connection)
|
public StarfaceApiClient(SystemConnection connection)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
|
|
||||||
@@ -112,9 +123,9 @@ namespace StarfaceOutlookSync.Services
|
|||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<StarfaceAddressBook>> GetAddressBooksAsync()
|
public async Task<List<RemoteAddressBook>> GetAddressBooksAsync()
|
||||||
{
|
{
|
||||||
var books = new List<StarfaceAddressBook>();
|
var books = new List<RemoteAddressBook>();
|
||||||
|
|
||||||
// Alle Tags laden - die Starface nutzt Tags als Adressbuch-Zuordnung
|
// Alle Tags laden - die Starface nutzt Tags als Adressbuch-Zuordnung
|
||||||
var allTags = new JArray();
|
var allTags = new JArray();
|
||||||
@@ -134,7 +145,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
// Zentrales Adressbuch (folder/all)
|
// Zentrales Adressbuch (folder/all)
|
||||||
var allTag = allTags.FirstOrDefault(t => t["name"]?.ToString() == "folder/all"
|
var allTag = allTags.FirstOrDefault(t => t["name"]?.ToString() == "folder/all"
|
||||||
|| t["alias"]?.ToString()?.Contains("folder.all") == true);
|
|| t["alias"]?.ToString()?.Contains("folder.all") == true);
|
||||||
books.Add(new StarfaceAddressBook
|
books.Add(new RemoteAddressBook
|
||||||
{
|
{
|
||||||
Type = "central",
|
Type = "central",
|
||||||
TagId = allTag?["id"]?.ToString() ?? "",
|
TagId = allTag?["id"]?.ToString() ?? "",
|
||||||
@@ -149,7 +160,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
(t["name"]?.ToString() == "folder/private" || t["alias"]?.ToString()?.Contains("folder.private") == true)
|
(t["name"]?.ToString() == "folder/private" || t["alias"]?.ToString()?.Contains("folder.private") == true)
|
||||||
&& t["owner"]?.ToString() == userId);
|
&& t["owner"]?.ToString() == userId);
|
||||||
|
|
||||||
books.Add(new StarfaceAddressBook
|
books.Add(new RemoteAddressBook
|
||||||
{
|
{
|
||||||
Type = "user",
|
Type = "user",
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
@@ -165,7 +176,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
// folder/all und folder/private bereits oben erfasst
|
// folder/all und folder/private bereits oben erfasst
|
||||||
if (tagName == "folder/all" || tagName == "folder/private") continue;
|
if (tagName == "folder/all" || tagName == "folder/private") continue;
|
||||||
|
|
||||||
books.Add(new StarfaceAddressBook
|
books.Add(new RemoteAddressBook
|
||||||
{
|
{
|
||||||
Type = "tag",
|
Type = "tag",
|
||||||
TagId = tag["id"]?.ToString() ?? "",
|
TagId = tag["id"]?.ToString() ?? "",
|
||||||
@@ -178,7 +189,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
|
|
||||||
public event Action<string> OnDebug;
|
public event Action<string> OnDebug;
|
||||||
|
|
||||||
public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book)
|
public async Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book)
|
||||||
{
|
{
|
||||||
var contacts = new List<UnifiedContact>();
|
var contacts = new List<UnifiedContact>();
|
||||||
int page = 0;
|
int page = 0;
|
||||||
@@ -314,7 +325,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
$"Synchronisation abgebrochen, um Dubletten zu vermeiden.");
|
$"Synchronisation abgebrochen, um Dubletten zu vermeiden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
|
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book)
|
||||||
{
|
{
|
||||||
var sfContact = MapToStarface(contact);
|
var sfContact = MapToStarface(contact);
|
||||||
|
|
||||||
@@ -354,7 +365,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
/// Hash-Baseline benoetigt, damit der Kontakt beim naechsten Sync nicht
|
/// Hash-Baseline benoetigt, damit der Kontakt beim naechsten Sync nicht
|
||||||
/// erneut faelschlich als geaendert gilt.
|
/// erneut faelschlich als geaendert gilt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book)
|
public async Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, RemoteAddressBook book)
|
||||||
{
|
{
|
||||||
var sfContact = MapToStarface(contact);
|
var sfContact = MapToStarface(contact);
|
||||||
sfContact["id"] = contactId;
|
sfContact["id"] = contactId;
|
||||||
@@ -394,11 +405,16 @@ namespace StarfaceOutlookSync.Services
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
return await GetContactAsync(contactId) ?? contact;
|
return await GetContactAsync(contactId, book) ?? contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.</summary>
|
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.
|
||||||
public async Task<UnifiedContact> GetContactAsync(string contactId)
|
///
|
||||||
|
/// Das Adressbuch spielt hier keine Rolle: Die Starface findet einen
|
||||||
|
/// Kontakt ueber alle Buecher hinweg. Genau darauf beruht die Regel,
|
||||||
|
/// dass ein nicht in der Liste gefundener Kontakt nur verschoben und
|
||||||
|
/// nicht geloescht ist.</summary>
|
||||||
|
public async Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -115,6 +115,48 @@ namespace StarfaceOutlookSync.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt eine beliebige exklusive Aktion (z.B. Dubletten zusammenfuehren)
|
||||||
|
/// unter demselben Schutz wie ein Sync aus: lokaler Guard + clientueber-
|
||||||
|
/// greifende Lock-Datei. Gibt false zurueck, wenn gerade ein Sync/eine
|
||||||
|
/// Aktion laeuft oder ein anderer Arbeitsplatz aktiv ist.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status)
|
||||||
|
{
|
||||||
|
// Auf einen gerade laufenden Sync warten (bis 60s), statt sofort
|
||||||
|
// abzubrechen - der Auto-Sync feuert ja jede Minute.
|
||||||
|
var guardDeadline = DateTime.UtcNow.AddSeconds(60);
|
||||||
|
while (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow >= guardDeadline)
|
||||||
|
{
|
||||||
|
status?.Invoke("Es laeuft bereits ein Sync - bitte spaeter erneut versuchen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
status?.Invoke("Warte, bis der laufende Sync fertig ist...");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncLock crossLock = null;
|
||||||
|
var sharedDir = UserSettings.Load().SharedDirectory;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
crossLock = await AcquireCrossClientLock(sharedDir, status);
|
||||||
|
if (crossLock == null)
|
||||||
|
{
|
||||||
|
status?.Invoke("Anderer Arbeitsplatz ist gerade aktiv - abgebrochen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
crossLock?.Dispose();
|
||||||
|
Interlocked.Exchange(ref _running, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SyncLock> AcquireCrossClientLock(string dir, Action<string> status)
|
private async Task<SyncLock> AcquireCrossClientLock(string dir, Action<string> status)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(dir))
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
|
|||||||
@@ -36,69 +36,6 @@ namespace StarfaceOutlookSync.Services
|
|||||||
m.LastSyncHash = "";
|
m.LastSyncHash = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Findet einen passenden Kontakt in der Kandidatenliste.
|
|
||||||
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
|
|
||||||
/// auf der anderen auch gefuellt (und gleich) sein.
|
|
||||||
/// Ein leeres Feld auf einer Seite und ein gefuelltes auf der anderen
|
|
||||||
/// bedeutet: verschiedene Kontakte.
|
|
||||||
/// </summary>
|
|
||||||
private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
|
|
||||||
{
|
|
||||||
if (candidates == null || candidates.Count == 0) return null;
|
|
||||||
|
|
||||||
foreach (var c in candidates)
|
|
||||||
{
|
|
||||||
if (IsMatch(contact, c))
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsMatch(UnifiedContact a, UnifiedContact b)
|
|
||||||
{
|
|
||||||
bool hasName = (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName))
|
|
||||||
&& (!string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName));
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Widerspruch: beide haben eine E-Mail, aber unterschiedlich -> verschiedene Personen.
|
|
||||||
bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch;
|
|
||||||
|
|
||||||
// Gleiche E-Mail ist der staerkste Identifikator und reicht allein.
|
|
||||||
if (emailMatch) return true;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Schwacher Pfad: Telefon/Fax nur zusammen mit gleicher Firma und ohne E-Mail-Widerspruch.
|
|
||||||
if (phoneMatch && companyMatch && !emailContradiction) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizePhone(string phone)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(phone)) return "";
|
|
||||||
// Nur Ziffern und + behalten
|
|
||||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
||||||
{
|
{
|
||||||
var result = new SyncResult
|
var result = new SyncResult
|
||||||
@@ -110,7 +47,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Log("Verbinde mit Starface...");
|
Log("Verbinde mit Starface...");
|
||||||
using (var starface = new StarfaceApiClient(profile.StarfaceConnection))
|
using (var starface = ContactBackendFactory.Create(profile.Connection))
|
||||||
{
|
{
|
||||||
starface.OnDebug += (msg) => Log(msg);
|
starface.OnDebug += (msg) => Log(msg);
|
||||||
var loginOk = await starface.LoginAsync();
|
var loginOk = await starface.LoginAsync();
|
||||||
@@ -127,7 +64,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
Log($"{outlookContacts.Count} Outlook-Kontakte geladen");
|
Log($"{outlookContacts.Count} Outlook-Kontakte geladen");
|
||||||
|
|
||||||
Log("Lade Starface-Kontakte...");
|
Log("Lade Starface-Kontakte...");
|
||||||
var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook);
|
var starfaceContacts = await starface.GetContactsAsync(profile.AddressBook);
|
||||||
Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
|
Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
|
||||||
|
|
||||||
// Bestehende Mappings laden
|
// Bestehende Mappings laden
|
||||||
@@ -172,7 +109,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
{
|
{
|
||||||
// Outlook-Kontakt nicht gefunden.
|
// Outlook-Kontakt nicht gefunden.
|
||||||
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
|
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
|
||||||
var reMatch = FindMatch(sc, outlookContacts.Where(c =>
|
var reMatch = ContactMatcher.FindMatch(sc, outlookContacts.Where(c =>
|
||||||
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
|
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
|
||||||
if (reMatch != null)
|
if (reMatch != null)
|
||||||
{
|
{
|
||||||
@@ -236,7 +173,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
// (sonst Dublette).
|
// (sonst Dublette).
|
||||||
// (b) per ID 404 -> in Starface WIRKLICH geloescht.
|
// (b) per ID 404 -> in Starface WIRKLICH geloescht.
|
||||||
bool stillExists = !string.IsNullOrEmpty(mapping.StarfaceId)
|
bool stillExists = !string.IsNullOrEmpty(mapping.StarfaceId)
|
||||||
&& await starface.GetContactAsync(mapping.StarfaceId) != null;
|
&& await starface.GetContactAsync(mapping.StarfaceId, profile.AddressBook) != null;
|
||||||
|
|
||||||
if (stillExists)
|
if (stillExists)
|
||||||
{
|
{
|
||||||
@@ -324,7 +261,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
|
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
|
||||||
{
|
{
|
||||||
// Outlook hat sich geaendert -> Starface updaten
|
// Outlook hat sich geaendert -> Starface updaten
|
||||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.AddressBook);
|
||||||
if (updated != null)
|
if (updated != null)
|
||||||
{
|
{
|
||||||
SetBaseline(mapping, oc, updated);
|
SetBaseline(mapping, oc, updated);
|
||||||
@@ -355,7 +292,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
var (merged, conflicts) = ContactMerger.Merge(
|
var (merged, conflicts) = ContactMerger.Merge(
|
||||||
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
|
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
|
||||||
|
|
||||||
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.StarfaceAddressBook);
|
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.AddressBook);
|
||||||
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
|
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
|
||||||
|
|
||||||
if (updatedSf != null || updatedOl != null)
|
if (updatedSf != null || updatedOl != null)
|
||||||
@@ -382,7 +319,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
{
|
{
|
||||||
// Fallback ohne Snapshot bzw. OutlookToStarface:
|
// Fallback ohne Snapshot bzw. OutlookToStarface:
|
||||||
// Outlook gewinnt komplett.
|
// Outlook gewinnt komplett.
|
||||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.AddressBook);
|
||||||
if (updated != null)
|
if (updated != null)
|
||||||
{
|
{
|
||||||
SetBaseline(mapping, oc, updated);
|
SetBaseline(mapping, oc, updated);
|
||||||
@@ -428,11 +365,11 @@ namespace StarfaceOutlookSync.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Duplikat-Check: existiert der Kontakt schon in der Starface?
|
// Duplikat-Check: existiert der Kontakt schon in der Starface?
|
||||||
var match = FindMatch(oc, unmappedStarface);
|
var match = ContactMatcher.FindMatch(oc, unmappedStarface);
|
||||||
if (match != null)
|
if (match != null)
|
||||||
{
|
{
|
||||||
// Existiert schon -> verknuepfen und updaten
|
// Existiert schon -> verknuepfen und updaten
|
||||||
var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook);
|
var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.AddressBook);
|
||||||
if (updated != null)
|
if (updated != null)
|
||||||
{
|
{
|
||||||
newMappings.Add(new SyncMapping
|
newMappings.Add(new SyncMapping
|
||||||
@@ -455,7 +392,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
{
|
{
|
||||||
// Neu -> in Starface erstellen
|
// Neu -> in Starface erstellen
|
||||||
Log($" Erstelle in Starface: {oc.DisplayName}");
|
Log($" Erstelle in Starface: {oc.DisplayName}");
|
||||||
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
|
var created = await starface.CreateContactAsync(oc, profile.AddressBook);
|
||||||
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
|
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
|
||||||
{
|
{
|
||||||
newMappings.Add(new SyncMapping
|
newMappings.Add(new SyncMapping
|
||||||
@@ -508,7 +445,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Duplikat-Check: existiert der Kontakt schon in Outlook?
|
// Duplikat-Check: existiert der Kontakt schon in Outlook?
|
||||||
var match = FindMatch(sc, unmappedOutlook);
|
var match = ContactMatcher.FindMatch(sc, unmappedOutlook);
|
||||||
if (match != null)
|
if (match != null)
|
||||||
{
|
{
|
||||||
// Existiert schon -> verknuepfen und updaten
|
// Existiert schon -> verknuepfen und updaten
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
|
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
|
||||||
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
|
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
|
||||||
<Product>Starface Outlook Sync</Product>
|
<Product>Starface Outlook Sync</Product>
|
||||||
<Version>0.0.0.30</Version>
|
<Version>0.0.3.3</Version>
|
||||||
<AssemblyVersion>0.0.0.30</AssemblyVersion>
|
<AssemblyVersion>0.0.3.3</AssemblyVersion>
|
||||||
<FileVersion>0.0.0.30</FileVersion>
|
<FileVersion>0.0.3.3</FileVersion>
|
||||||
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
|
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
|
||||||
<Copyright>Stefan Hacker - HackerSoft</Copyright>
|
<Copyright>Stefan Hacker - HackerSoft</Copyright>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
|
|
||||||
var lblVersion = new Label
|
var lblVersion = new Label
|
||||||
{
|
{
|
||||||
Text = "Version 0.0.0.30",
|
Text = "Version 0.0.3.3",
|
||||||
Left = 0, Top = 56, Width = 340, Height = 20,
|
Left = 0, Top = 56, Width = 340, Height = 20,
|
||||||
TextAlign = ContentAlignment.MiddleCenter,
|
TextAlign = ContentAlignment.MiddleCenter,
|
||||||
ForeColor = Color.Gray
|
ForeColor = Color.Gray
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
using StarfaceOutlookSync.Services;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dialog zum Zusammenfuehren doppelter Kontakte fuer EIN Profil.
|
||||||
|
/// Analysiert zuerst (Vorschau), fuehrt nach Bestaetigung unter derselben
|
||||||
|
/// Sperre wie ein Sync zusammen.
|
||||||
|
/// </summary>
|
||||||
|
public class DedupeForm : Form
|
||||||
|
{
|
||||||
|
private readonly SyncProfile _profile;
|
||||||
|
private readonly DedupeService _dedupe = new DedupeService();
|
||||||
|
private readonly SyncCoordinator _coordinator = new SyncCoordinator();
|
||||||
|
|
||||||
|
private Label _lblInfo, _lblSummary;
|
||||||
|
private CheckBox _chkDetail;
|
||||||
|
private TextBox _txtLog;
|
||||||
|
private Button _btnStart, _btnClose;
|
||||||
|
|
||||||
|
private DedupeService.DedupePlan _plan;
|
||||||
|
private bool _busy;
|
||||||
|
|
||||||
|
public DedupeForm(SyncProfile profile)
|
||||||
|
{
|
||||||
|
_profile = profile;
|
||||||
|
_dedupe.OnProgress += AppendLog;
|
||||||
|
InitializeComponent();
|
||||||
|
Load += async (s, e) => await Analyze();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
Text = $"Dubletten zusammenfuehren - {_profile.Name}";
|
||||||
|
Size = new Size(640, 500);
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
Font = new Font("Segoe UI", 9);
|
||||||
|
MinimizeBox = false;
|
||||||
|
|
||||||
|
_lblInfo = new Label
|
||||||
|
{
|
||||||
|
Text = $"Profil: {_profile.Name} ({_profile.Connection.System} <-> {_profile.OutlookFolderName})",
|
||||||
|
Left = 12, Top = 12, Width = 600, Height = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
_lblSummary = new Label
|
||||||
|
{
|
||||||
|
Text = "Analysiere...",
|
||||||
|
Left = 12, Top = 36, Width = 600, Height = 40, ForeColor = Color.DimGray
|
||||||
|
};
|
||||||
|
|
||||||
|
_chkDetail = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Detailvorschau anzeigen (welche Kontakte zusammengefuehrt werden)",
|
||||||
|
Left = 12, Top = 80, AutoSize = true, Enabled = false
|
||||||
|
};
|
||||||
|
_chkDetail.CheckedChanged += (s, e) => { if (_chkDetail.Checked) RenderDetail(); };
|
||||||
|
|
||||||
|
_txtLog = new TextBox
|
||||||
|
{
|
||||||
|
Left = 12, Top = 108, Width = 604, Height = 300,
|
||||||
|
Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Both, WordWrap = false,
|
||||||
|
BackColor = Color.FromArgb(30, 30, 30), ForeColor = Color.FromArgb(212, 212, 212),
|
||||||
|
Font = new Font("Consolas", 9),
|
||||||
|
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
_btnStart = new Button
|
||||||
|
{
|
||||||
|
Text = "Zusammenfuehren starten", Left = 12, Top = 420, Width = 190, Height = 30,
|
||||||
|
Enabled = false, Anchor = AnchorStyles.Bottom | AnchorStyles.Left
|
||||||
|
};
|
||||||
|
_btnStart.Click += async (s, e) => await Execute();
|
||||||
|
|
||||||
|
_btnClose = new Button
|
||||||
|
{
|
||||||
|
Text = "Schliessen", Left = 526, Top = 420, Width = 90, Height = 30,
|
||||||
|
DialogResult = DialogResult.Cancel, Anchor = AnchorStyles.Bottom | AnchorStyles.Right
|
||||||
|
};
|
||||||
|
|
||||||
|
Controls.AddRange(new Control[] { _lblInfo, _lblSummary, _chkDetail, _txtLog, _btnStart, _btnClose });
|
||||||
|
CancelButton = _btnClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendLog(string message)
|
||||||
|
{
|
||||||
|
// Der Aufruf kann aus dem Hintergrund kommen, nachdem das Fenster
|
||||||
|
// bereits geschlossen/entsorgt wurde -> defensiv absichern.
|
||||||
|
if (IsDisposed || Disposing) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (InvokeRequired) { BeginInvoke(new Action(() => AppendLog(message))); return; }
|
||||||
|
if (_txtLog == null || _txtLog.IsDisposed) return;
|
||||||
|
_txtLog.AppendText(message + "\r\n");
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException) { }
|
||||||
|
catch (InvalidOperationException) { } // Handle noch nicht/nicht mehr da
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
|
{
|
||||||
|
// Nicht schliessen, solange Analyse/Zusammenfuehrung laeuft - sonst
|
||||||
|
// greifen die Hintergrund-Fortsetzungen auf entsorgte Controls zu.
|
||||||
|
if (_busy)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
_lblSummary.Text = "Bitte warten - der Vorgang laeuft noch...";
|
||||||
|
_lblSummary.ForeColor = Color.OrangeRed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
base.OnFormClosing(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Analyze()
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
_btnStart.Enabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_plan = await Task.Run(() => _dedupe.AnalyzeAsync(_profile));
|
||||||
|
|
||||||
|
var s = $"Telefonanlage: {_plan.RemoteGroups.Count} Gruppe(n) mit Dubletten, {_plan.RemoteDeletions} Kontakt(e) werden zusammengefuehrt.\r\n" +
|
||||||
|
$"Outlook: {_plan.OutlookGroups.Count} Gruppe(n) mit Dubletten, {_plan.OutlookDeletions} Kontakt(e) werden zusammengefuehrt.";
|
||||||
|
_lblSummary.Text = _plan.HasWork ? s : "Keine Dubletten gefunden. Alles sauber. ✓";
|
||||||
|
_lblSummary.ForeColor = _plan.HasWork ? Color.OrangeRed : Color.Green;
|
||||||
|
|
||||||
|
_chkDetail.Enabled = _plan.HasWork;
|
||||||
|
_btnStart.Enabled = _plan.HasWork;
|
||||||
|
if (_chkDetail.Checked) RenderDetail();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_lblSummary.Text = "Analyse fehlgeschlagen: " + ex.Message;
|
||||||
|
_lblSummary.ForeColor = Color.Red;
|
||||||
|
AppendLog("FEHLER: " + ex.Message);
|
||||||
|
}
|
||||||
|
finally { _busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderDetail()
|
||||||
|
{
|
||||||
|
if (_plan == null) return;
|
||||||
|
AppendLog("");
|
||||||
|
AppendLog("=== Vorschau: Telefonanlage ===");
|
||||||
|
foreach (var g in _plan.RemoteGroups)
|
||||||
|
AppendLog($" Behalten: '{g.Survivor.DisplayName}' <= {g.Duplicates.Count} Dublette(n)");
|
||||||
|
AppendLog("=== Vorschau: Outlook ===");
|
||||||
|
foreach (var g in _plan.OutlookGroups)
|
||||||
|
AppendLog($" Behalten: '{g.Survivor.DisplayName}' <= {g.Duplicates.Count} Dublette(n)");
|
||||||
|
AppendLog("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Execute()
|
||||||
|
{
|
||||||
|
if (_busy || _plan == null || !_plan.HasWork) return;
|
||||||
|
|
||||||
|
var confirm = MessageBox.Show(this,
|
||||||
|
$"Jetzt zusammenfuehren?\n\n" +
|
||||||
|
$"Telefonanlage: {_plan.RemoteDeletions} Dublette(n) werden geloescht.\n" +
|
||||||
|
$"Outlook: {_plan.OutlookDeletions} Dublette(n) werden geloescht.\n\n" +
|
||||||
|
$"Die Daten der Dubletten werden vorher in den behaltenen Kontakt uebernommen (leere Felder aufgefuellt).\n" +
|
||||||
|
$"Danach bitte einmal synchronisieren.",
|
||||||
|
"Dubletten zusammenfuehren", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||||
|
if (confirm != DialogResult.Yes) return;
|
||||||
|
|
||||||
|
_busy = true;
|
||||||
|
_btnStart.Enabled = false;
|
||||||
|
_btnClose.Enabled = false;
|
||||||
|
AppendLog("Starte Zusammenfuehrung...");
|
||||||
|
Logger.Log($"Dubletten-Zusammenfuehrung gestartet: Profil '{_profile.Name}'");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DedupeService.DedupeReport report = null;
|
||||||
|
bool ran = await _coordinator.RunExclusiveAsync(
|
||||||
|
async () => { report = await Task.Run(() => _dedupe.ExecuteAsync(_profile)); },
|
||||||
|
status: AppendLog);
|
||||||
|
|
||||||
|
if (!ran)
|
||||||
|
{
|
||||||
|
AppendLog("Abgebrochen: gerade laeuft ein Sync (oder ein anderer Arbeitsplatz ist aktiv).");
|
||||||
|
_lblSummary.Text = "Nicht ausgefuehrt - es laeuft gerade ein Sync. Bitte kurz warten und erneut versuchen.";
|
||||||
|
_lblSummary.ForeColor = Color.OrangeRed;
|
||||||
|
MessageBox.Show(this,
|
||||||
|
"Die Zusammenfuehrung wurde NICHT ausgefuehrt, weil gerade ein Sync laeuft " +
|
||||||
|
"(oder ein anderer Arbeitsplatz aktiv ist).\n\nBitte kurz warten und erneut auf " +
|
||||||
|
"'Zusammenfuehren starten' klicken.",
|
||||||
|
"Dubletten", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
_btnStart.Enabled = true; // erneuter Versuch moeglich
|
||||||
|
}
|
||||||
|
else if (report != null)
|
||||||
|
{
|
||||||
|
var summary = $"Fertig. Anlage: {report.RemoteMerged} Gruppe(n), {report.RemoteDeleted} geloescht. " +
|
||||||
|
$"Outlook: {report.OutlookMerged} Gruppe(n), {report.OutlookDeleted} geloescht. " +
|
||||||
|
$"Fehler: {report.Errors}.";
|
||||||
|
AppendLog(summary);
|
||||||
|
foreach (var m in report.Messages) AppendLog(" " + m);
|
||||||
|
AppendLog("Bitte jetzt einmal synchronisieren, damit die Zuordnungen sauber neu aufgebaut werden.");
|
||||||
|
Logger.Log("Dubletten-Zusammenfuehrung: " + summary);
|
||||||
|
foreach (var m in report.Messages) Logger.Log(" " + m);
|
||||||
|
|
||||||
|
_lblSummary.Text = summary + " -> Jetzt synchronisieren.";
|
||||||
|
_lblSummary.ForeColor = report.Errors > 0 ? Color.OrangeRed : Color.Green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog("FEHLER: " + ex.Message);
|
||||||
|
Logger.Log($"Dubletten-Zusammenfuehrung FEHLER '{_profile.Name}': {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_busy = false;
|
||||||
|
_btnClose.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
private NotifyIcon _trayIcon;
|
private NotifyIcon _trayIcon;
|
||||||
private ContextMenuStrip _trayMenu;
|
private ContextMenuStrip _trayMenu;
|
||||||
private ListView _profileList;
|
private ListView _profileList;
|
||||||
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnLog, _btnInfo;
|
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnDedupe, _btnReset, _btnSettings, _btnLog, _btnInfo;
|
||||||
private StatusStrip _statusBar;
|
private StatusStrip _statusBar;
|
||||||
private ToolStripStatusLabel _statusLabel;
|
private ToolStripStatusLabel _statusLabel;
|
||||||
private Timer _autoSyncTimer;
|
private Timer _autoSyncTimer;
|
||||||
@@ -83,8 +83,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
Text = "Starface Kontakt-Sync";
|
Text = "Starface Kontakt-Sync";
|
||||||
Size = new Size(830, 450);
|
Size = new Size(930, 450);
|
||||||
MinimumSize = new Size(830, 350);
|
MinimumSize = new Size(930, 350);
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
Font = new Font("Segoe UI", 9);
|
Font = new Font("Segoe UI", 9);
|
||||||
Icon = AppIcon.GetIcon();
|
Icon = AppIcon.GetIcon();
|
||||||
@@ -126,6 +126,9 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
|
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
|
||||||
_btnSync.Click += async (s, e) => await SyncSelectedProfile();
|
_btnSync.Click += async (s, e) => await SyncSelectedProfile();
|
||||||
|
|
||||||
|
_btnDedupe = new Button { Text = "Dubletten", Width = 85, Height = 30 };
|
||||||
|
_btnDedupe.Click += (s, e) => ShowDedupe();
|
||||||
|
|
||||||
_btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
|
_btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
|
||||||
_btnReset.Click += (s, e) => ResetSync();
|
_btnReset.Click += (s, e) => ResetSync();
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
|
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
|
||||||
_btnInfo.Click += (s, e) => ShowAbout();
|
_btnInfo.Click += (s, e) => ShowAbout();
|
||||||
|
|
||||||
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnLog, _btnInfo });
|
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnDedupe, _btnReset, _btnSettings, _btnLog, _btnInfo });
|
||||||
|
|
||||||
// Statusbar
|
// Statusbar
|
||||||
_statusBar = new StatusStrip();
|
_statusBar = new StatusStrip();
|
||||||
@@ -241,7 +244,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
var item = new ListViewItem(new[]
|
var item = new ListViewItem(new[]
|
||||||
{
|
{
|
||||||
p.Name,
|
p.Name,
|
||||||
$"{p.StarfaceConnection.Host} / {p.StarfaceAddressBook.Name}",
|
$"{p.Connection.Host} / {p.AddressBook.Name}",
|
||||||
p.OutlookFolderName,
|
p.OutlookFolderName,
|
||||||
dirText,
|
dirText,
|
||||||
lastSync
|
lastSync
|
||||||
@@ -355,9 +358,11 @@ namespace StarfaceOutlookSync.UI
|
|||||||
{
|
{
|
||||||
// Lokaler Guard, Lock-Datei, Konflikt-Notizen und Protokoll laufen
|
// Lokaler Guard, Lock-Datei, Konflikt-Notizen und Protokoll laufen
|
||||||
// zentral im Coordinator (gemeinsam mit dem manuellen Sync-Pfad).
|
// zentral im Coordinator (gemeinsam mit dem manuellen Sync-Pfad).
|
||||||
|
// WICHTIG: die Engine im Hintergrund laufen lassen, damit das
|
||||||
|
// Fenster und das Tray-Kontextmenue waehrend des Syncs reagieren.
|
||||||
var outcome = await _coordinator.RunAsync(
|
var outcome = await _coordinator.RunAsync(
|
||||||
profile,
|
profile,
|
||||||
runEngine: () => _syncEngine.SyncProfileAsync(profile),
|
runEngine: () => Task.Run(() => _syncEngine.SyncProfileAsync(profile)),
|
||||||
status: SetStatus);
|
status: SetStatus);
|
||||||
|
|
||||||
if (outcome.Skipped)
|
if (outcome.Skipped)
|
||||||
@@ -423,7 +428,13 @@ namespace StarfaceOutlookSync.UI
|
|||||||
private void Balloon(int ms, string title, string text, ToolTipIcon icon, bool warn)
|
private void Balloon(int ms, string title, string text, ToolTipIcon icon, bool warn)
|
||||||
{
|
{
|
||||||
bool allow = warn ? _notifyWarn : _notifyGeneral;
|
bool allow = warn ? _notifyWarn : _notifyGeneral;
|
||||||
if (allow) _trayIcon.ShowBalloonTip(ms, title, text, icon);
|
if (!allow) return;
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
BeginInvoke(new Action(() => _trayIcon.ShowBalloonTip(ms, title, text, icon)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_trayIcon.ShowBalloonTip(ms, title, text, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetStatus(string text)
|
private void SetStatus(string text)
|
||||||
@@ -458,6 +469,24 @@ namespace StarfaceOutlookSync.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowDedupe()
|
||||||
|
{
|
||||||
|
if (_profileList.SelectedItems.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Bitte zuerst ein Profil auswaehlen.", "Dubletten",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var profile = _profileList.SelectedItems[0].Tag as SyncProfile;
|
||||||
|
if (profile == null) return;
|
||||||
|
|
||||||
|
using (var form = new DedupeForm(profile))
|
||||||
|
{
|
||||||
|
form.ShowDialog(this);
|
||||||
|
}
|
||||||
|
RefreshProfileList();
|
||||||
|
}
|
||||||
|
|
||||||
private void ExitApplication()
|
private void ExitApplication()
|
||||||
{
|
{
|
||||||
_autoSyncTimer?.Stop();
|
_autoSyncTimer?.Stop();
|
||||||
|
|||||||
@@ -24,10 +24,30 @@ namespace StarfaceOutlookSync.UI
|
|||||||
private Label _lblTestResult;
|
private Label _lblTestResult;
|
||||||
private Label _lblDirectionHint;
|
private Label _lblDirectionHint;
|
||||||
|
|
||||||
private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>();
|
private List<RemoteAddressBook> _addressBooks = new List<RemoteAddressBook>();
|
||||||
private List<string> _outlookFolderPaths = new List<string>();
|
private List<string> _outlookFolderPaths = new List<string>();
|
||||||
private string _selectedOutlookPath = "";
|
private string _selectedOutlookPath = "";
|
||||||
private string _selectedOutlookName = "";
|
private string _selectedOutlookName = "";
|
||||||
|
private ComboBox _cmbSystem;
|
||||||
|
private Label _lblAnlage;
|
||||||
|
private Label _lblLoginId;
|
||||||
|
private Label _lblAddressBook;
|
||||||
|
|
||||||
|
private string AnlagenName => GewaehlteAnlage == PhoneSystem.Fonaria ? "Fonaria" : "Starface";
|
||||||
|
|
||||||
|
private PhoneSystem GewaehlteAnlage =>
|
||||||
|
_cmbSystem != null && _cmbSystem.SelectedIndex == 1 ? PhoneSystem.Fonaria : PhoneSystem.Starface;
|
||||||
|
|
||||||
|
/// <summary>Beschriftungen, die je nach Anlage anders heissen.</summary>
|
||||||
|
private void UpdateSystemLabels()
|
||||||
|
{
|
||||||
|
var fonaria = GewaehlteAnlage == PhoneSystem.Fonaria;
|
||||||
|
if (_lblLoginId != null)
|
||||||
|
_lblLoginId.Text = fonaria ? "Benutzername:" : "Login-ID:";
|
||||||
|
if (_lblAddressBook != null)
|
||||||
|
_lblAddressBook.Text = fonaria ? "Fonaria-Adressbuch:" : "Starface-Adressbuch:";
|
||||||
|
UpdateDirectionHint();
|
||||||
|
}
|
||||||
|
|
||||||
public ProfileEditorForm(SyncProfile profile)
|
public ProfileEditorForm(SyncProfile profile)
|
||||||
{
|
{
|
||||||
@@ -54,8 +74,26 @@ namespace StarfaceOutlookSync.UI
|
|||||||
panel.Controls.Add(MakeLabel("Profilname:", 12, y)); y += 22;
|
panel.Controls.Add(MakeLabel("Profilname:", 12, y)); y += 22;
|
||||||
_txtName = new TextBox { Left = 12, Top = y, Width = 420 }; panel.Controls.Add(_txtName); y += 32;
|
_txtName = new TextBox { Left = 12, Top = y, Width = 420 }; panel.Controls.Add(_txtName); y += 32;
|
||||||
|
|
||||||
// === Starface ===
|
// === Anlage ===
|
||||||
panel.Controls.Add(MakeSectionLabel("Starface-Verbindung", 12, y)); y += 26;
|
_lblAnlage = MakeSectionLabel("Verbindung zur Telefonanlage", 12, y); y += 26;
|
||||||
|
panel.Controls.Add(_lblAnlage);
|
||||||
|
|
||||||
|
panel.Controls.Add(MakeLabel("Anlage:", 12, y)); y += 22;
|
||||||
|
_cmbSystem = new ComboBox { Left = 12, Top = y, Width = 200, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
|
_cmbSystem.Items.AddRange(new object[] { "Starface", "Fonaria" });
|
||||||
|
_cmbSystem.SelectedIndex = 0;
|
||||||
|
// Beide sprechen ihre eigene Schnittstelle; der Vorgabeport ist
|
||||||
|
// derselbe, aber die Adressbuecher heissen anders. Beim Wechsel
|
||||||
|
// muss die Auswahl darum neu geladen werden.
|
||||||
|
_cmbSystem.SelectedIndexChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
_addressBooks.Clear();
|
||||||
|
_cmbAddressBook.Items.Clear();
|
||||||
|
_lblTestResult.Text = "Anlage gewechselt - bitte Adressbuecher neu laden.";
|
||||||
|
_lblTestResult.ForeColor = Color.FromArgb(150, 80, 0);
|
||||||
|
UpdateSystemLabels();
|
||||||
|
};
|
||||||
|
panel.Controls.Add(_cmbSystem); y += 32;
|
||||||
|
|
||||||
panel.Controls.Add(MakeLabel("Host / IP-Adresse:", 12, y)); y += 22;
|
panel.Controls.Add(MakeLabel("Host / IP-Adresse:", 12, y)); y += 22;
|
||||||
_txtHost = new TextBox { Left = 12, Top = y, Width = 320 };
|
_txtHost = new TextBox { Left = 12, Top = y, Width = 320 };
|
||||||
@@ -67,7 +105,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_chkSsl = new CheckBox { Text = "HTTPS verwenden", Left = 12, Top = y, Checked = true, AutoSize = true };
|
_chkSsl = new CheckBox { Text = "HTTPS verwenden", Left = 12, Top = y, Checked = true, AutoSize = true };
|
||||||
panel.Controls.Add(_chkSsl); y += 28;
|
panel.Controls.Add(_chkSsl); y += 28;
|
||||||
|
|
||||||
panel.Controls.Add(MakeLabel("Login-ID:", 12, y)); y += 22;
|
_lblLoginId = MakeLabel("Login-ID:", 12, y);
|
||||||
|
panel.Controls.Add(_lblLoginId); y += 22;
|
||||||
_txtLoginId = new TextBox { Left = 12, Top = y, Width = 200 }; panel.Controls.Add(_txtLoginId); y += 32;
|
_txtLoginId = new TextBox { Left = 12, Top = y, Width = 200 }; panel.Controls.Add(_txtLoginId); y += 32;
|
||||||
|
|
||||||
panel.Controls.Add(MakeLabel("Kennwort:", 12, y)); y += 22;
|
panel.Controls.Add(MakeLabel("Kennwort:", 12, y)); y += 22;
|
||||||
@@ -84,7 +123,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_lblTestResult = new Label { Left = 12, Top = y, Width = 420, Height = 20, ForeColor = Color.Gray };
|
_lblTestResult = new Label { Left = 12, Top = y, Width = 420, Height = 20, ForeColor = Color.Gray };
|
||||||
panel.Controls.Add(_lblTestResult); y += 26;
|
panel.Controls.Add(_lblTestResult); y += 26;
|
||||||
|
|
||||||
panel.Controls.Add(MakeLabel("Starface-Adressbuch:", 12, y)); y += 22;
|
_lblAddressBook = MakeLabel("Adressbuch der Anlage:", 12, y);
|
||||||
|
panel.Controls.Add(_lblAddressBook); y += 22;
|
||||||
_cmbAddressBook = new ComboBox { Left = 12, Top = y, Width = 420, DropDownStyle = ComboBoxStyle.DropDownList };
|
_cmbAddressBook = new ComboBox { Left = 12, Top = y, Width = 420, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
panel.Controls.Add(_cmbAddressBook); y += 32;
|
panel.Controls.Add(_cmbAddressBook); y += 32;
|
||||||
|
|
||||||
@@ -100,7 +140,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
|
|
||||||
panel.Controls.Add(MakeLabel("Sync-Richtung:", 12, y)); y += 22;
|
panel.Controls.Add(MakeLabel("Sync-Richtung:", 12, y)); y += 22;
|
||||||
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
|
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
_cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Starface", "Starface -> Outlook" });
|
_cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Anlage", "Anlage -> Outlook" });
|
||||||
_cmbDirection.SelectedIndex = 0;
|
_cmbDirection.SelectedIndex = 0;
|
||||||
panel.Controls.Add(_cmbDirection); y += 28;
|
panel.Controls.Add(_cmbDirection); y += 28;
|
||||||
|
|
||||||
@@ -138,11 +178,11 @@ namespace StarfaceOutlookSync.UI
|
|||||||
{
|
{
|
||||||
switch (_cmbDirection.SelectedIndex)
|
switch (_cmbDirection.SelectedIndex)
|
||||||
{
|
{
|
||||||
case 1: // Outlook -> Starface
|
case 1: // Outlook -> Anlage
|
||||||
_lblDirectionHint.Text = "Achtung: Das Starface-Adressbuch wird zur exakten Kopie von Outlook.\nKontakte, die nur in Starface existieren, werden geloescht.";
|
_lblDirectionHint.Text = $"Achtung: Das Adressbuch der {AnlagenName} wird zur exakten Kopie von Outlook.\nKontakte, die nur dort existieren, werden geloescht.";
|
||||||
break;
|
break;
|
||||||
case 2: // Starface -> Outlook
|
case 2: // Anlage -> Outlook
|
||||||
_lblDirectionHint.Text = "Achtung: Der Outlook-Ordner wird zur exakten Kopie von Starface.\nKontakte, die nur in Outlook existieren, werden geloescht.";
|
_lblDirectionHint.Text = $"Achtung: Der Outlook-Ordner wird zur exakten Kopie der {AnlagenName}.\nKontakte, die nur in Outlook existieren, werden geloescht.";
|
||||||
break;
|
break;
|
||||||
default: // Bidirektional
|
default: // Bidirektional
|
||||||
_lblDirectionHint.Text = "Aenderungen werden in beide Richtungen abgeglichen.";
|
_lblDirectionHint.Text = "Aenderungen werden in beide Richtungen abgeglichen.";
|
||||||
@@ -182,15 +222,17 @@ namespace StarfaceOutlookSync.UI
|
|||||||
if (_existingProfile != null)
|
if (_existingProfile != null)
|
||||||
{
|
{
|
||||||
_txtName.Text = _existingProfile.Name;
|
_txtName.Text = _existingProfile.Name;
|
||||||
_txtHost.Text = _existingProfile.StarfaceConnection.Host;
|
_cmbSystem.SelectedIndex = _existingProfile.Connection.System == PhoneSystem.Fonaria ? 1 : 0;
|
||||||
_txtPort.Text = _existingProfile.StarfaceConnection.Port.ToString();
|
_txtHost.Text = _existingProfile.Connection.Host;
|
||||||
_chkSsl.Checked = _existingProfile.StarfaceConnection.UseSsl;
|
_txtPort.Text = _existingProfile.Connection.Port.ToString();
|
||||||
_txtLoginId.Text = _existingProfile.StarfaceConnection.LoginId;
|
_chkSsl.Checked = _existingProfile.Connection.UseSsl;
|
||||||
_txtPassword.Text = _existingProfile.StarfaceConnection.Password;
|
_txtLoginId.Text = _existingProfile.Connection.LoginId;
|
||||||
|
_txtPassword.Text = _existingProfile.Connection.Password;
|
||||||
_chkEnabled.Checked = _existingProfile.Enabled;
|
_chkEnabled.Checked = _existingProfile.Enabled;
|
||||||
_numAutoSync.Value = _existingProfile.AutoSyncIntervalMinutes;
|
_numAutoSync.Value = _existingProfile.AutoSyncIntervalMinutes;
|
||||||
|
|
||||||
_cmbDirection.SelectedIndex = (int)_existingProfile.SyncDirection;
|
_cmbDirection.SelectedIndex = (int)_existingProfile.SyncDirection;
|
||||||
|
UpdateSystemLabels();
|
||||||
|
|
||||||
// Outlook-Ordner
|
// Outlook-Ordner
|
||||||
_selectedOutlookPath = _existingProfile.OutlookFolderPath;
|
_selectedOutlookPath = _existingProfile.OutlookFolderPath;
|
||||||
@@ -198,10 +240,10 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_txtOutlookFolder.Text = _selectedOutlookPath;
|
_txtOutlookFolder.Text = _selectedOutlookPath;
|
||||||
|
|
||||||
// Adressbuch
|
// Adressbuch
|
||||||
if (_existingProfile.StarfaceAddressBook != null)
|
if (_existingProfile.AddressBook != null)
|
||||||
{
|
{
|
||||||
_addressBooks.Add(_existingProfile.StarfaceAddressBook);
|
_addressBooks.Add(_existingProfile.AddressBook);
|
||||||
_cmbAddressBook.Items.Add(_existingProfile.StarfaceAddressBook.Name);
|
_cmbAddressBook.Items.Add(_existingProfile.AddressBook.Name);
|
||||||
_cmbAddressBook.SelectedIndex = 0;
|
_cmbAddressBook.SelectedIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,10 +269,11 @@ namespace StarfaceOutlookSync.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private StarfaceConnection GetConnection()
|
private SystemConnection GetConnection()
|
||||||
{
|
{
|
||||||
return new StarfaceConnection
|
return new SystemConnection
|
||||||
{
|
{
|
||||||
|
System = GewaehlteAnlage,
|
||||||
Host = _txtHost.Text.Trim(),
|
Host = _txtHost.Text.Trim(),
|
||||||
Port = int.TryParse(_txtPort.Text, out var p) ? p : 443,
|
Port = int.TryParse(_txtPort.Text, out var p) ? p : 443,
|
||||||
UseSsl = _chkSsl.Checked,
|
UseSsl = _chkSsl.Checked,
|
||||||
@@ -247,7 +290,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var client = new StarfaceApiClient(GetConnection()))
|
using (var client = ContactBackendFactory.Create(GetConnection()))
|
||||||
{
|
{
|
||||||
var ok = await client.LoginAsync();
|
var ok = await client.LoginAsync();
|
||||||
if (ok)
|
if (ok)
|
||||||
@@ -280,7 +323,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var client = new StarfaceApiClient(GetConnection()))
|
using (var client = ContactBackendFactory.Create(GetConnection()))
|
||||||
{
|
{
|
||||||
var ok = await client.LoginAsync();
|
var ok = await client.LoginAsync();
|
||||||
if (!ok)
|
if (!ok)
|
||||||
@@ -350,8 +393,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
{
|
{
|
||||||
Id = _existingProfile?.Id ?? _pm.GenerateId(),
|
Id = _existingProfile?.Id ?? _pm.GenerateId(),
|
||||||
Name = _txtName.Text.Trim(),
|
Name = _txtName.Text.Trim(),
|
||||||
StarfaceConnection = GetConnection(),
|
Connection = GetConnection(),
|
||||||
StarfaceAddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
|
AddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
|
||||||
OutlookFolderPath = _selectedOutlookPath,
|
OutlookFolderPath = _selectedOutlookPath,
|
||||||
OutlookFolderName = _selectedOutlookName,
|
OutlookFolderName = _selectedOutlookName,
|
||||||
SyncDirection = (SyncDirection)_cmbDirection.SelectedIndex,
|
SyncDirection = (SyncDirection)_cmbDirection.SelectedIndex,
|
||||||
@@ -367,8 +410,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Wenn Adressbuch gewechselt wurde, Mappings zuruecksetzen
|
// Wenn Adressbuch gewechselt wurde, Mappings zuruecksetzen
|
||||||
if (_existingProfile.StarfaceAddressBook?.TagId != profile.StarfaceAddressBook?.TagId
|
if (_existingProfile.AddressBook?.TagId != profile.AddressBook?.TagId
|
||||||
|| _existingProfile.StarfaceAddressBook?.Type != profile.StarfaceAddressBook?.Type)
|
|| _existingProfile.AddressBook?.Type != profile.AddressBook?.Type)
|
||||||
{
|
{
|
||||||
_pm.SaveMappings(profile.Id, new List<SyncMapping>());
|
_pm.SaveMappings(profile.Id, new List<SyncMapping>());
|
||||||
profile.LastSync = "";
|
profile.LastSync = "";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
|
|
||||||
var infoLabel = new Label
|
var infoLabel = new Label
|
||||||
{
|
{
|
||||||
Text = $"{_profile.StarfaceConnection.Host} ({_profile.StarfaceAddressBook.Name}) <-> {_profile.OutlookFolderName}",
|
Text = $"{_profile.Connection.Host} ({_profile.AddressBook.Name}) <-> {_profile.OutlookFolderName}",
|
||||||
Left = 12, Top = 12, Width = 460, AutoSize = false, Height = 20
|
Left = 12, Top = 12, Width = 460, AutoSize = false, Height = 20
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user