Compare commits

..

No commits in common. "main" and "v0.0.0.7" have entirely different histories.

13 changed files with 283 additions and 1074 deletions

View File

@ -42,32 +42,6 @@ Windows-Anwendung zur bidirektionalen Synchronisation von Kontakten zwischen Mic
5. Starface-Adressbuch und Outlook-Kontaktordner waehlen 5. Starface-Adressbuch und Outlook-Kontaktordner waehlen
6. Speichern und "Jetzt synchronisieren" 6. Speichern und "Jetzt synchronisieren"
### Outlook-Sicherheitsabfrage unterdruecken
Beim Zugriff auf Outlook-Kontakte zeigt Outlook standardmaessig einen
Sicherheitsdialog ("Ein Programm versucht auf Ihre E-Mail-Adressinformationen
zuzugreifen"). Dieser kann in den Einstellungen der App deaktiviert werden:
1. In der App auf "Einstellungen" klicken
2. "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern
**Auf Domaenen-PCs / Terminal Servern:**
Die Outlook-Sicherheitseinstellungen werden dort per Gruppenrichtlinie (GPO)
gesteuert und sind im Trust Center ausgegraut. In diesem Fall muss die App
**einmalig als Administrator** gestartet werden, damit die Registry-Keys
unter HKLM geschrieben werden koennen:
1. Rechtsklick auf die App -> "Als Administrator ausfuehren"
2. Einstellungen -> "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern und App schliessen
4. Outlook neu starten
5. App kann danach wieder normal (ohne Admin) gestartet werden
Die Einstellung bleibt dauerhaft bestehen und gilt fuer alle Benutzer
auf dem Rechner.
### Deinstallation ### Deinstallation
Ueber Windows Einstellungen -> Apps oder die Systemsteuerung. Ueber Windows Einstellungen -> Apps oder die Systemsteuerung.

View File

@ -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.23" #define MyAppVersion "0.0.0.7"
#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"

View File

@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using Microsoft.Win32;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace StarfaceOutlookSync.Models namespace StarfaceOutlookSync.Models
@ -8,8 +7,6 @@ namespace StarfaceOutlookSync.Models
public class UserSettings public class UserSettings
{ {
public bool StartMinimized { get; set; } = false; public bool StartMinimized { get; set; } = false;
public bool SyncOnStart { get; set; } = false;
public bool AutoAcceptOutlookPrompt { get; set; } = false;
private static readonly string SettingsFile = Path.Combine( private static readonly string SettingsFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@ -35,86 +32,6 @@ namespace StarfaceOutlookSync.Models
File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented)); File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented));
} }
catch { } catch { }
ApplyOutlookSecuritySetting();
}
public void ApplyOutlookSecuritySetting()
{
var versions = new[] { "16.0", "15.0" };
var securityValues = new (string name, int value)[]
{
("ObjectModelGuard", 2),
("PromptOOMAddressBookAccess", 2),
("PromptOOMAddressInformationAccess", 2),
("PromptOOMSend", 2),
("PromptOOMSaveAs", 2),
("PromptOOMFormulaAccess", 2),
("PromptOOMCustomAction", 2),
("PromptSimpleMAPISend", 2),
("PromptSimpleMAPINameResolve", 2),
("PromptSimpleMAPIOpenMessage", 2),
("AdminSecurityMode", 3),
};
// In alle moeglichen Pfade schreiben (HKCU + HKLM, Policies + direkt)
var roots = new[] { Registry.CurrentUser, Registry.LocalMachine };
var prefixes = new[]
{
@"Software\Policies\Microsoft\Office",
@"Software\Microsoft\Office"
};
foreach (var ver in versions)
{
foreach (var root in roots)
{
foreach (var prefix in prefixes)
{
var regPath = $@"{prefix}\{ver}\Outlook\Security";
try
{
if (AutoAcceptOutlookPrompt)
{
var key = root.CreateSubKey(regPath);
if (key != null)
{
foreach (var (name, value) in securityValues)
key.SetValue(name, value, RegistryValueKind.DWord);
key.Close();
}
}
else
{
try { root.DeleteSubKey(regPath, false); } catch { }
}
}
catch { } // Kein Fehler wenn Rechte fehlen - naechsten Pfad versuchen
}
}
}
}
/// <summary>
/// Prueft ob die Outlook-Sicherheitseinstellung per GPO blockiert wird.
/// </summary>
public static bool IsOutlookSecurityLockedByPolicy()
{
try
{
// Wenn HKLM Policies gesetzt sind und wir dort nicht schreiben koennen
var key = Registry.LocalMachine.OpenSubKey(
@"Software\Policies\Microsoft\Office\16.0\Outlook\Security", false);
if (key != null)
{
var val = key.GetValue("AdminSecurityMode");
key.Close();
if (val != null) return true;
}
}
catch { }
return false;
} }
} }
} }

View File

@ -153,32 +153,6 @@ namespace StarfaceOutlookSync.Services
} }
} }
private dynamic FindFolderByPath(dynamic folder, string targetPath)
{
try
{
string currentPath = folder.FolderPath;
if (currentPath == targetPath)
return folder;
var subs = folder.Folders;
for (int i = 1; i <= (int)subs.Count; i++)
{
try
{
var sub = subs[i];
var match = FindFolderByPath(sub, targetPath);
if (match != null) return match;
Marshal.ReleaseComObject(sub);
}
catch { }
}
Marshal.ReleaseComObject(subs);
}
catch { }
return null;
}
private dynamic GetFolderByPath(string folderPath) private dynamic GetFolderByPath(string folderPath)
{ {
var app = GetOutlookApp(); var app = GetOutlookApp();
@ -189,69 +163,51 @@ namespace StarfaceOutlookSync.Services
try try
{ {
// Versuch 1: Alle Kontaktordner durchsuchen und per FolderPath matchen var parts = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
dynamic current = null;
var stores = ns.Stores; var stores = ns.Stores;
for (int i = 1; i <= (int)stores.Count; i++) for (int i = 1; i <= (int)stores.Count; i++)
{ {
try var store = stores[i];
var root = store.GetRootFolder();
string rootName = root.Name;
if (rootName == parts[0])
{ {
var store = stores[i]; current = root;
var root = store.GetRootFolder(); break;
var match = FindFolderByPath(root, folderPath);
if (match != null)
{
Marshal.ReleaseComObject(stores);
return match;
}
Marshal.ReleaseComObject(root);
Marshal.ReleaseComObject(store);
} }
catch { } Marshal.ReleaseComObject(root);
Marshal.ReleaseComObject(store);
} }
Marshal.ReleaseComObject(stores); Marshal.ReleaseComObject(stores);
// Versuch 2: Namespace.Folders direkt navigieren if (current == null)
var parts = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); return ns.GetDefaultFolder(OlFolderContacts);
if (parts.Length >= 1)
for (int i = 1; i < parts.Length; i++)
{ {
var topFolders = ns.Folders; bool found = false;
for (int i = 1; i <= (int)topFolders.Count; i++) var subFolders = current.Folders;
for (int j = 1; j <= (int)subFolders.Count; j++)
{ {
var topFolder = topFolders[i]; var sub = subFolders[j];
if ((string)topFolder.Name == parts[0]) if ((string)sub.Name == parts[i])
{ {
dynamic current = topFolder; current = sub;
for (int p = 1; p < parts.Length; p++) found = true;
{ break;
bool found = false;
var subs = current.Folders;
for (int j = 1; j <= (int)subs.Count; j++)
{
var sub = subs[j];
if ((string)sub.Name == parts[p])
{
current = sub;
found = true;
break;
}
Marshal.ReleaseComObject(sub);
}
Marshal.ReleaseComObject(subs);
if (!found) { current = null; break; }
}
if (current != null)
{
Marshal.ReleaseComObject(topFolders);
return current;
}
} }
Marshal.ReleaseComObject(topFolder); Marshal.ReleaseComObject(sub);
} }
Marshal.ReleaseComObject(topFolders); Marshal.ReleaseComObject(subFolders);
if (!found)
return ns.GetDefaultFolder(OlFolderContacts);
} }
System.Diagnostics.Debug.WriteLine($"Folder not found by path: {folderPath}, using default"); return current;
return ns.GetDefaultFolder(OlFolderContacts);
} }
catch catch
{ {
@ -265,51 +221,24 @@ namespace StarfaceOutlookSync.Services
try try
{ {
var folder = GetFolderByPath(folderPath); var folder = GetFolderByPath(folderPath);
System.Diagnostics.Debug.WriteLine($"Reading contacts from: {(string)folder.FolderPath}");
var items = folder.Items; var items = folder.Items;
int count = (int)items.Count;
System.Diagnostics.Debug.WriteLine($"Items count: {count}");
for (int i = 1; i <= count; i++) for (int i = 1; i <= (int)items.Count; i++)
{ {
dynamic item = null; dynamic item = null;
try try
{ {
item = items[i]; item = items[i];
int itemClass = -1; // Nur ContactItems verarbeiten (Class = 40 = olContact)
try { itemClass = (int)item.Class; } catch { } if ((int)item.Class == 40)
// olContact = 40, aber manche Kontakte melden Class anders
// Versuch einfach die Kontakt-Felder zu lesen
if (itemClass == 40)
{ {
contacts.Add(MapFromOutlook(item)); contacts.Add(MapFromOutlook(item));
} }
else
{
// Fallback: Pruefen ob es trotzdem ein Kontakt ist
try
{
string eid = item.EntryID;
string fn = item.FirstName;
string ln = item.LastName;
// Hat Kontakt-Felder -> ist ein Kontakt
contacts.Add(MapFromOutlook(item));
}
catch
{
// Kein Kontakt, ueberspringen
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error reading item {i}: {ex.Message}");
} }
catch { }
finally finally
{ {
if (item != null) try { Marshal.ReleaseComObject(item); } catch { } if (item != null) Marshal.ReleaseComObject(item);
} }
} }
@ -394,40 +323,29 @@ namespace StarfaceOutlookSync.Services
} }
} }
private static string SafeGet(dynamic ci, string property)
{
try
{
object val = ci.GetType().InvokeMember(property,
System.Reflection.BindingFlags.GetProperty, null, ci, null);
return val?.ToString() ?? "";
}
catch { return ""; }
}
private UnifiedContact MapFromOutlook(dynamic ci) private UnifiedContact MapFromOutlook(dynamic ci)
{ {
return new UnifiedContact return new UnifiedContact
{ {
OutlookEntryId = SafeGet(ci, "EntryID"), OutlookEntryId = (string)(ci.EntryID ?? ""),
FirstName = SafeGet(ci, "FirstName"), FirstName = (string)(ci.FirstName ?? ""),
LastName = SafeGet(ci, "LastName"), LastName = (string)(ci.LastName ?? ""),
Company = SafeGet(ci, "CompanyName"), Company = (string)(ci.CompanyName ?? ""),
JobTitle = SafeGet(ci, "JobTitle"), JobTitle = (string)(ci.JobTitle ?? ""),
Email = SafeGet(ci, "Email1Address"), Email = (string)(ci.Email1Address ?? ""),
EmailSecondary = SafeGet(ci, "Email2Address"), EmailSecondary = (string)(ci.Email2Address ?? ""),
PhoneWork = SafeGet(ci, "BusinessTelephoneNumber"), PhoneWork = (string)(ci.BusinessTelephoneNumber ?? ""),
PhoneMobile = SafeGet(ci, "MobileTelephoneNumber"), PhoneMobile = (string)(ci.MobileTelephoneNumber ?? ""),
PhoneHome = SafeGet(ci, "HomeTelephoneNumber"), PhoneHome = (string)(ci.HomeTelephoneNumber ?? ""),
Fax = SafeGet(ci, "BusinessFaxNumber"), Fax = (string)(ci.BusinessFaxNumber ?? ""),
Street = SafeGet(ci, "BusinessAddressStreet"), Street = (string)(ci.BusinessAddressStreet ?? ""),
City = SafeGet(ci, "BusinessAddressCity"), City = (string)(ci.BusinessAddressCity ?? ""),
PostalCode = SafeGet(ci, "BusinessAddressPostalCode"), PostalCode = (string)(ci.BusinessAddressPostalCode ?? ""),
State = SafeGet(ci, "BusinessAddressState"), State = (string)(ci.BusinessAddressState ?? ""),
Country = SafeGet(ci, "BusinessAddressCountry"), Country = (string)(ci.BusinessAddressCountry ?? ""),
Website = SafeGet(ci, "WebPage"), Website = (string)(ci.WebPage ?? ""),
Notes = SafeGet(ci, "Body"), Notes = (string)(ci.Body ?? ""),
Salutation = SafeGet(ci, "Title"), Salutation = (string)(ci.Title ?? ""),
Birthday = GetBirthdayString(ci) Birthday = GetBirthdayString(ci)
}; };
} }
@ -436,9 +354,8 @@ namespace StarfaceOutlookSync.Services
{ {
try try
{ {
object val = ci.GetType().InvokeMember("Birthday", DateTime bday = ci.Birthday;
System.Reflection.BindingFlags.GetProperty, null, ci, null); if (bday.Year > 1900 && bday != DateTime.MinValue)
if (val is DateTime bday && bday.Year > 1900 && bday != DateTime.MinValue)
return bday.ToString("yyyy-MM-dd"); return bday.ToString("yyyy-MM-dd");
} }
catch { } catch { }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -116,74 +115,51 @@ namespace StarfaceOutlookSync.Services
{ {
var books = new List<StarfaceAddressBook>(); var books = new List<StarfaceAddressBook>();
// Alle Tags laden - die Starface nutzt Tags als Adressbuch-Zuordnung books.Add(new StarfaceAddressBook
var allTags = new JArray(); {
Type = "central",
Name = "Zentrales Adressbuch"
});
var userId = await GetCurrentUserIdAsync();
if (!string.IsNullOrEmpty(userId))
{
books.Add(new StarfaceAddressBook
{
Type = "user",
UserId = userId,
Name = "Persoenliches Adressbuch"
});
}
// Tags als virtuelle Adressbuecher
try try
{ {
var resp = await _http.GetAsync($"{_baseUrl}/contacts/tags"); var resp = await _http.GetAsync($"{_baseUrl}/contacts/tags");
if (resp.IsSuccessStatusCode) if (resp.IsSuccessStatusCode)
{ {
allTags = JArray.Parse(await resp.Content.ReadAsStringAsync()); var tags = JArray.Parse(await resp.Content.ReadAsStringAsync());
OnDebug?.Invoke($"Gefundene Tags: {allTags.Count}"); foreach (var tag in tags)
foreach (var t in allTags) {
OnDebug?.Invoke($" Tag: {t["name"]} (id: {t["id"]}, alias: {t["alias"]}, owner: {t["owner"]})"); books.Add(new StarfaceAddressBook
{
Type = "tag",
TagId = tag["id"]?.ToString() ?? "",
Name = $"Tag: {tag["name"]}"
});
}
} }
} }
catch { } catch { }
// Zentrales Adressbuch (folder/all)
var allTag = allTags.FirstOrDefault(t => t["name"]?.ToString() == "folder/all"
|| t["alias"]?.ToString()?.Contains("folder.all") == true);
books.Add(new StarfaceAddressBook
{
Type = "central",
TagId = allTag?["id"]?.ToString() ?? "",
Name = "Zentrales Adressbuch"
});
// Persoenliches Adressbuch (folder/private mit owner = userId)
var userId = await GetCurrentUserIdAsync();
if (!string.IsNullOrEmpty(userId))
{
var privateTag = allTags.FirstOrDefault(t =>
(t["name"]?.ToString() == "folder/private" || t["alias"]?.ToString()?.Contains("folder.private") == true)
&& t["owner"]?.ToString() == userId);
books.Add(new StarfaceAddressBook
{
Type = "user",
UserId = userId,
TagId = privateTag?["id"]?.ToString() ?? "",
Name = "Persoenliches Adressbuch"
});
}
// Alle weiteren Tags als Adressbuecher anbieten
foreach (var tag in allTags)
{
var tagName = tag["name"]?.ToString() ?? "";
// folder/all und folder/private bereits oben erfasst
if (tagName == "folder/all" || tagName == "folder/private") continue;
books.Add(new StarfaceAddressBook
{
Type = "tag",
TagId = tag["id"]?.ToString() ?? "",
Name = tagName
});
}
return books; return books;
} }
public event Action<string> OnDebug;
public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book) public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book)
{ {
var contacts = new List<UnifiedContact>(); var contacts = new List<UnifiedContact>();
int page = 0; int page = 0;
const int pageSize = 200; const int pageSize = 200;
bool firstPage = true;
while (true) while (true)
{ {
@ -196,68 +172,11 @@ namespace StarfaceOutlookSync.Services
var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}"); var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}");
if (!resp.IsSuccessStatusCode) break; if (!resp.IsSuccessStatusCode) break;
var body = await resp.Content.ReadAsStringAsync(); var array = JArray.Parse(await resp.Content.ReadAsStringAsync());
JArray array;
// Die API gibt je nach Version ein Array oder ein Objekt mit "items" zurueck
var token = JToken.Parse(body);
if (token is JArray directArray)
{
array = directArray;
}
else if (token is JObject obj)
{
// Versuche gaengige Felder: items, contacts, data, results
array = (obj["items"] ?? obj["contacts"] ?? obj["data"] ?? obj["results"]) as JArray;
if (array == null)
{
// Einzelnes Kontakt-Objekt? Dann in Array wrappen
if (obj["id"] != null && obj["blocks"] != null)
{
array = new JArray { obj };
}
else
{
System.Diagnostics.Debug.WriteLine($"Unerwartete Starface-Antwort: {body.Substring(0, Math.Min(200, body.Length))}");
break;
}
}
}
else
{
break;
}
if (array.Count == 0) break; if (array.Count == 0) break;
OnDebug?.Invoke($"Seite {page}: {array.Count} Kontakte in Liste");
// Die Listen-API gibt nur Summary zurueck.
// Jeden Kontakt einzeln abrufen fuer alle Felder.
foreach (var item in array) foreach (var item in array)
{ contacts.Add(MapFromStarface(item));
var id = item["id"]?.ToString();
if (string.IsNullOrEmpty(id)) continue;
try
{
var detailResp = await _http.GetAsync($"{_baseUrl}/contacts/{id}");
if (detailResp.IsSuccessStatusCode)
{
var detailBody = await detailResp.Content.ReadAsStringAsync();
var detailObj = JObject.Parse(detailBody);
if (firstPage)
{
OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{detailObj.ToString(Formatting.Indented)}");
firstPage = false;
}
contacts.Add(MapFromStarface(detailObj));
}
}
catch { }
}
if (array.Count < pageSize) break; if (array.Count < pageSize) break;
page++; page++;
@ -269,34 +188,15 @@ namespace StarfaceOutlookSync.Services
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book) public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
{ {
var sfContact = MapToStarface(contact); var sfContact = MapToStarface(contact);
// Tag zuweisen - die Starface verlangt dass jeder Kontakt einem Tag zugeordnet ist
if (!string.IsNullOrEmpty(book.TagId))
{
sfContact["tags"] = new JArray
{
new JObject { ["id"] = book.TagId }
};
}
var query = ""; var query = "";
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}"; query = $"?userId={book.UserId}";
var body = sfContact.ToString(); var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json");
OnDebug?.Invoke($"POST /contacts{query} Body:\n{body}");
var content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content); var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content);
var respBody = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) return null;
if (!resp.IsSuccessStatusCode) var created = JObject.Parse(await resp.Content.ReadAsStringAsync());
{
OnDebug?.Invoke($"POST /contacts fehlgeschlagen: {(int)resp.StatusCode} {resp.StatusCode}\n{respBody}");
return null;
}
var created = JObject.Parse(respBody);
return MapFromStarface(created); return MapFromStarface(created);
} }
@ -304,27 +204,12 @@ namespace StarfaceOutlookSync.Services
{ {
var sfContact = MapToStarface(contact); var sfContact = MapToStarface(contact);
sfContact["id"] = contactId; sfContact["id"] = contactId;
// Tag beibehalten
if (!string.IsNullOrEmpty(book.TagId))
{
sfContact["tags"] = new JArray { new JObject { ["id"] = book.TagId } };
}
var query = ""; var query = "";
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}"; query = $"?userId={book.UserId}";
var body = sfContact.ToString(); var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json");
var content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _http.PutAsync($"{_baseUrl}/contacts/{contactId}{query}", content); var resp = await _http.PutAsync($"{_baseUrl}/contacts/{contactId}{query}", content);
if (!resp.IsSuccessStatusCode)
{
var respBody = await resp.Content.ReadAsStringAsync();
OnDebug?.Invoke($"PUT /contacts/{contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
}
return resp.IsSuccessStatusCode; return resp.IsSuccessStatusCode;
} }
@ -339,10 +224,7 @@ namespace StarfaceOutlookSync.Services
var contact = new UnifiedContact(); var contact = new UnifiedContact();
contact.StarfaceId = item["id"]?.ToString() ?? ""; contact.StarfaceId = item["id"]?.ToString() ?? "";
// Attribute per "name"-Feld mappen (zuverlaessiger als displayKey, var attrs = new Dictionary<string, string>();
// weil viele Felder USER_DEFINED als displayKey haben)
var byName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var byDisplayKey = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var blocks = item["blocks"] as JArray; var blocks = item["blocks"] as JArray;
if (blocks != null) if (blocks != null)
{ {
@ -352,123 +234,79 @@ namespace StarfaceOutlookSync.Services
if (blockAttrs == null) continue; if (blockAttrs == null) continue;
foreach (var attr in blockAttrs) foreach (var attr in blockAttrs)
{ {
var name = attr["name"]?.ToString() ?? ""; var key = attr["displayKey"]?.ToString() ?? "";
var displayKey = attr["displayKey"]?.ToString() ?? "";
var val = attr["value"]?.ToString() ?? ""; var val = attr["value"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(val)) if (!string.IsNullOrEmpty(val))
{ attrs[key] = val;
if (!string.IsNullOrEmpty(name))
byName[name] = val;
// displayKey nur als Fallback (viele sind USER_DEFINED)
if (!string.IsNullOrEmpty(displayKey) && displayKey != "USER_DEFINED")
byDisplayKey[displayKey] = val;
}
} }
} }
} }
// Primaer nach name-Feld mappen, Fallback auf displayKey contact.FirstName = attrs.GetValueOrDefault("NAME", "");
string Get(string name, string displayKey = null) contact.LastName = attrs.GetValueOrDefault("SURNAME", "");
{ contact.Company = attrs.GetValueOrDefault("COMPANY", "");
if (byName.TryGetValue(name, out var v)) return v; contact.JobTitle = attrs.GetValueOrDefault("JOB_TITLE", "");
if (displayKey != null && byDisplayKey.TryGetValue(displayKey, out v)) return v; contact.Email = attrs.GetValueOrDefault("EMAIL", "");
return ""; contact.PhoneWork = attrs.GetValueOrDefault("OFFICE_PHONE_NUMBER", "");
} contact.PhoneMobile = attrs.GetValueOrDefault("MOBILE_PHONE_NUMBER", "");
contact.PhoneHome = attrs.GetValueOrDefault("PRIVATE_PHONE_NUMBER", "");
contact.FirstName = Get("firstname", "NAME"); contact.Fax = attrs.GetValueOrDefault("FAX_NUMBER", "");
contact.LastName = Get("familyname", "SURNAME"); contact.Street = attrs.GetValueOrDefault("STREET", "");
contact.Company = Get("company", "COMPANY"); contact.City = attrs.GetValueOrDefault("CITY", "");
contact.JobTitle = Get("jobtitle", "JOB_TITLE"); contact.PostalCode = attrs.GetValueOrDefault("POSTAL_CODE", "");
contact.Email = Get("e-mail", "EMAIL"); contact.State = attrs.GetValueOrDefault("STATE", "");
contact.PhoneWork = Get("phone", "PHONE_NUMBER"); contact.Country = attrs.GetValueOrDefault("COUNTRY", "");
contact.PhoneMobile = Get("mobile", "MOBILE_PHONE_NUMBER"); contact.Website = attrs.GetValueOrDefault("URL", "");
contact.PhoneHome = Get("homephone", "PRIVATE_PHONE_NUMBER"); contact.Notes = attrs.GetValueOrDefault("NOTE", "");
contact.Fax = Get("fax", "FAX_NUMBER"); contact.Salutation = attrs.GetValueOrDefault("SALUTATION", "");
contact.Street = Get("street", "STREET"); contact.Title = attrs.GetValueOrDefault("TITLE", "");
contact.City = Get("city", "CITY"); contact.Birthday = attrs.GetValueOrDefault("BIRTHDAY", "");
contact.PostalCode = Get("postcode", "POSTAL_CODE");
contact.State = Get("state", "STATE");
contact.Country = Get("country", "COUNTRY");
contact.Website = Get("url", "URL");
contact.Notes = Get("comment", "NOTE");
contact.Salutation = Get("salutation", "SALUTATION");
contact.Title = Get("title", "TITLE");
contact.Birthday = Get("birthday", "BIRTHDAY");
return contact; return contact;
} }
private JObject MapToStarface(UnifiedContact contact) private JObject MapToStarface(UnifiedContact contact)
{ {
JArray MakeAttrs(params (string displayKey, string name, string value)[] fields) var attrs = new JArray();
void AddAttr(string displayKey, string name, string value)
{ {
var arr = new JArray(); if (!string.IsNullOrEmpty(value))
foreach (var (dk, n, v) in fields) attrs.Add(new JObject { ["displayKey"] = displayKey, ["name"] = name, ["value"] = value });
{
if (!string.IsNullOrEmpty(v))
arr.Add(new JObject { ["displayKey"] = dk, ["name"] = n, ["value"] = v });
}
return arr;
} }
// Block-Struktur wie von der Starface erwartet AddAttr("NAME", "firstName", contact.FirstName);
var contactBlock = MakeAttrs( AddAttr("SURNAME", "lastName", contact.LastName);
("NAME", "firstname", contact.FirstName), AddAttr("COMPANY", "company", contact.Company);
("SURNAME", "familyname", contact.LastName), AddAttr("JOB_TITLE", "jobTitle", contact.JobTitle);
("COMPANY", "company", contact.Company) AddAttr("EMAIL", "email", contact.Email);
); AddAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.PhoneWork);
AddAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.PhoneMobile);
var addressBlock = MakeAttrs( AddAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.PhoneHome);
("USER_DEFINED", "street", contact.Street), AddAttr("FAX_NUMBER", "fax", contact.Fax);
("POSTAL_CODE", "postcode", contact.PostalCode), AddAttr("STREET", "street", contact.Street);
("USER_DEFINED", "city", contact.City), AddAttr("CITY", "city", contact.City);
("USER_DEFINED", "state", contact.State), AddAttr("POSTAL_CODE", "postalCode", contact.PostalCode);
("USER_DEFINED", "country", contact.Country) AddAttr("STATE", "state", contact.State);
); AddAttr("COUNTRY", "country", contact.Country);
AddAttr("URL", "website", contact.Website);
var phoneBlock = MakeAttrs( AddAttr("NOTE", "notes", contact.Notes);
("PHONE_NUMBER", "phone", contact.PhoneWork), AddAttr("SALUTATION", "salutation", contact.Salutation);
("PRIVATE_PHONE_NUMBER", "homephone", contact.PhoneHome), AddAttr("TITLE", "title", contact.Title);
("MOBILE_PHONE_NUMBER", "mobile", contact.PhoneMobile), AddAttr("BIRTHDAY", "birthday", contact.Birthday);
("FAX_NUMBER", "fax", contact.Fax)
);
var emailBlock = MakeAttrs(
("EMAIL", "e-mail", contact.Email),
("URL", "url", contact.Website),
("USER_DEFINED", "comment", contact.Notes)
);
var blocks = new JArray();
blocks.Add(new JObject
{
["name"] = "contact",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_contact",
["attributes"] = contactBlock
});
blocks.Add(new JObject
{
["name"] = "address",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_address",
["attributes"] = addressBlock
});
blocks.Add(new JObject
{
["name"] = "telephone",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_telephone",
["attributes"] = phoneBlock
});
blocks.Add(new JObject
{
["name"] = "email",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_email",
["attributes"] = emailBlock
});
return new JObject return new JObject
{ {
["id"] = contact.StarfaceId ?? "", ["id"] = contact.StarfaceId ?? "",
["blocks"] = blocks ["blocks"] = new JArray
{
new JObject
{
["name"] = "contact",
["resourceKey"] = "contact",
["attributes"] = attrs
}
}
}; };
} }

View File

@ -15,103 +15,30 @@ namespace StarfaceOutlookSync.Services
private void Log(string message) => OnProgress?.Invoke(message); private void Log(string message) => OnProgress?.Invoke(message);
/// <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) private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
{ {
if (candidates == null || candidates.Count == 0) return null; // Erst E-Mail-Match
if (!string.IsNullOrEmpty(contact.Email))
foreach (var c in candidates)
{ {
if (IsMatch(contact, c)) var byEmail = candidates.FirstOrDefault(c =>
return c; !string.IsNullOrEmpty(c.Email) &&
c.Email.Equals(contact.Email, StringComparison.OrdinalIgnoreCase));
if (byEmail != null) return byEmail;
}
// Dann Name-Match
if (!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName))
{
var byName = candidates.FirstOrDefault(c =>
c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) &&
c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) &&
(!string.IsNullOrEmpty(c.FirstName) || !string.IsNullOrEmpty(c.LastName)));
if (byName != null) return byName;
} }
return null; return null;
} }
private static bool IsMatch(UnifiedContact a, UnifiedContact b)
{
// Mindestens ein identifizierendes Feld muss vorhanden sein
bool hasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
bool hasEmail = !string.IsNullOrEmpty(a.Email);
bool hasPhone = !string.IsNullOrEmpty(a.PhoneWork) || !string.IsNullOrEmpty(a.PhoneMobile);
if (!hasName && !hasEmail && !hasPhone) return false;
// E-Mail: wenn auf beiden Seiten vorhanden, muss sie gleich sein
// Wenn nur auf einer Seite vorhanden -> kein Match
if (!FieldsCompatible(a.Email, b.Email)) return false;
// Name: wenn auf einer Seite vorhanden, muss er gleich sein
if (!FieldsCompatible(a.FirstName, b.FirstName)) return false;
if (!FieldsCompatible(a.LastName, b.LastName)) return false;
// Firma: wenn auf einer Seite vorhanden, muss sie gleich sein
// Leere Firma vs. gefuellte Firma = verschiedene Kontakte
if (!FieldsCompatible(a.Company, b.Company)) return false;
// Telefon/Fax: wenn auf einer Seite vorhanden, muss es gleich sein
if (!PhoneFieldsCompatible(a.PhoneWork, b.PhoneWork)) return false;
if (!PhoneFieldsCompatible(a.PhoneMobile, b.PhoneMobile)) return false;
if (!PhoneFieldsCompatible(a.PhoneHome, b.PhoneHome)) return false;
if (!PhoneFieldsCompatible(a.Fax, b.Fax)) return false;
// Mindestens ein starkes Match muss vorhanden sein
bool emailMatch = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email)
&& a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase);
bool nameMatch = hasName
&& a.FirstName.Equals(b.FirstName, StringComparison.OrdinalIgnoreCase)
&& a.LastName.Equals(b.LastName, StringComparison.OrdinalIgnoreCase)
&& (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName));
bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork)
&& NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork))
|| (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax)
&& NormalizePhone(a.Fax) == NormalizePhone(b.Fax));
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
// Email oder Name reicht. Telefon/Fax nur mit Firma zusammen.
return emailMatch || nameMatch || (phoneMatch && companyMatch) || (companyMatch && phoneMatch);
}
/// <summary>
/// Prueft ob zwei Felder kompatibel sind.
/// Beide leer = kompatibel. Beide gleich = kompatibel.
/// Eins leer, eins gefuellt = NICHT kompatibel (verschiedene Kontakte).
/// </summary>
private static bool FieldsCompatible(string a, string b)
{
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return true;
if (aEmpty != bEmpty) return false; // Einer leer, anderer nicht
return a.Equals(b, StringComparison.OrdinalIgnoreCase);
}
private static bool PhoneFieldsCompatible(string a, string b)
{
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return true;
if (aEmpty != bEmpty) return false;
return NormalizePhone(a) == NormalizePhone(b);
}
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
@ -122,10 +49,10 @@ namespace StarfaceOutlookSync.Services
try try
{ {
// Starface verbinden
Log("Verbinde mit Starface..."); Log("Verbinde mit Starface...");
using (var starface = new StarfaceApiClient(profile.StarfaceConnection)) using (var starface = new StarfaceApiClient(profile.StarfaceConnection))
{ {
starface.OnDebug += (msg) => Log(msg);
var loginOk = await starface.LoginAsync(); var loginOk = await starface.LoginAsync();
if (!loginOk) if (!loginOk)
{ {
@ -134,6 +61,10 @@ namespace StarfaceOutlookSync.Services
return result; return result;
} }
var mappings = _profileManager.GetMappings(profile.Id);
var mappingByOutlook = mappings.ToDictionary(m => m.OutlookEntryId, m => m);
var mappingByStarface = mappings.ToDictionary(m => m.StarfaceId, m => m);
// Kontakte laden // Kontakte laden
Log("Lade Outlook-Kontakte..."); Log("Lade Outlook-Kontakte...");
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath); var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
@ -143,290 +74,144 @@ namespace StarfaceOutlookSync.Services
var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook); var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook);
Log($"{starfaceContacts.Count} Starface-Kontakte geladen"); Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
// Bestehende Mappings laden // Outlook -> Starface
var mappings = _profileManager.GetMappings(profile.Id); if (profile.SyncDirection == SyncDirection.Both ||
profile.SyncDirection == SyncDirection.OutlookToStarface)
// Sets fuer schnellen Lookup
var mappingByOutlook = new Dictionary<string, SyncMapping>();
var mappingByStarface = new Dictionary<string, SyncMapping>();
foreach (var m in mappings)
{ {
if (!string.IsNullOrEmpty(m.OutlookEntryId)) Log("Synchronisiere Outlook -> Starface...");
mappingByOutlook[m.OutlookEntryId] = m; foreach (var oc in outlookContacts)
if (!string.IsNullOrEmpty(m.StarfaceId))
mappingByStarface[m.StarfaceId] = m;
}
// Tracking: welche Kontakte wurden bereits verarbeitet
var processedStarfaceIds = new HashSet<string>();
var processedOutlookIds = new HashSet<string>();
var newMappings = new List<SyncMapping>();
// ============================================
// Phase 1: Bestehende Mappings abgleichen
// ============================================
Log("Gleiche bestehende Zuordnungen ab...");
foreach (var mapping in mappings.ToList())
{
var oc = outlookContacts.FirstOrDefault(c => c.OutlookEntryId == mapping.OutlookEntryId);
var sc = starfaceContacts.FirstOrDefault(c => c.StarfaceId == mapping.StarfaceId);
if (oc != null) processedOutlookIds.Add(oc.OutlookEntryId);
if (sc != null) processedStarfaceIds.Add(sc.StarfaceId);
if (oc == null && sc == null)
{
// Beide Seiten geloescht -> Mapping entfernen
Log($" Mapping verwaist (beide geloescht), entferne");
continue;
}
if (oc == null && sc != null)
{
// Outlook-Kontakt nicht gefunden.
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
var reMatch = FindMatch(sc, outlookContacts.Where(c =>
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
if (reMatch != null)
{
// Kontakt existiert noch in Outlook, nur EntryID geaendert
Log($" EntryID geaendert, verknuepfe neu: {sc.DisplayName}");
mapping.OutlookEntryId = reMatch.OutlookEntryId;
processedOutlookIds.Add(reMatch.OutlookEntryId);
newMappings.Add(mapping);
continue;
}
// Wirklich geloescht -> in Starface auch loeschen
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)
{
if (await starface.DeleteContactAsync(mapping.StarfaceId))
{
result.Updated++;
Log($" Geloescht (OL->SF): {sc.DisplayName}");
}
}
else
{
newMappings.Add(mapping);
}
continue;
}
if (oc != null && sc == null)
{
// Starface-Kontakt nicht gefunden.
// Kann passieren wenn der Kontakt einem anderen Adressbuch gehoert.
// NICHT loeschen, nur Mapping entfernen - wird in Phase 2/3 neu verknuepft
Log($" Starface-Kontakt nicht in Liste (anderes Adressbuch?): {oc.DisplayName}");
// Mapping verwerfen, Outlook-Kontakt als unverarbeitet belassen
// damit er in Phase 2 neu zugeordnet oder erstellt werden kann
continue;
}
if (oc != null && sc != null)
{
// Beide vorhanden -> auf Aenderungen pruefen
var olHash = oc.GetHash();
var sfHash = sc.GetHash();
bool olChanged = olHash != mapping.LastSyncHash;
bool sfChanged = sfHash != mapping.LastSyncHash;
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
{
// Outlook hat sich geaendert -> Starface updaten
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook))
{
mapping.LastSyncHash = olHash;
result.Updated++;
Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
}
}
else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook))
{
// Starface hat sich geaendert -> Outlook updaten
if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc))
{
mapping.LastSyncHash = sfHash;
result.Updated++;
Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
}
}
else if (olChanged && sfChanged)
{
// Beide geaendert -> Konflikt, neuere gewinnt (Outlook bevorzugt)
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
{
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook))
{
mapping.LastSyncHash = olHash;
result.Updated++;
Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
}
}
else
{
if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc))
{
mapping.LastSyncHash = sfHash;
result.Updated++;
Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
}
}
}
// Beide unveraendert -> nichts tun
}
newMappings.Add(mapping);
}
// ============================================
// Phase 2: Neue Outlook-Kontakte (ohne Mapping)
// ============================================
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)
{
var unmappedOutlook = outlookContacts
.Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId))
.ToList();
if (unmappedOutlook.Count > 0)
Log($"Neue Outlook-Kontakte: {unmappedOutlook.Count}");
// Starface-Kontakte die noch kein Mapping haben (fuer Duplikat-Check)
var unmappedStarface = starfaceContacts
.Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId))
.ToList();
foreach (var oc in unmappedOutlook)
{ {
try try
{ {
// Duplikat-Check: existiert der Kontakt schon in der Starface? SyncMapping existing = null;
var match = FindMatch(oc, unmappedStarface); if (!string.IsNullOrEmpty(oc.OutlookEntryId))
if (match != null) mappingByOutlook.TryGetValue(oc.OutlookEntryId, out existing);
if (existing != null)
{ {
// Existiert schon -> verknuepfen und updaten var hash = oc.GetHash();
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook)) if (hash != existing.LastSyncHash)
{ {
newMappings.Add(new SyncMapping if (await starface.UpdateContactAsync(existing.StarfaceId, oc, profile.StarfaceAddressBook))
{ {
ProfileId = profile.Id, existing.LastSyncHash = hash;
OutlookEntryId = oc.OutlookEntryId, result.Updated++;
StarfaceId = match.StarfaceId, }
LastSyncHash = oc.GetHash()
});
processedStarfaceIds.Add(match.StarfaceId);
unmappedStarface.Remove(match);
result.Updated++;
Log($" Verknuepft (OL->SF): {oc.DisplayName}");
} }
} }
else else
{ {
// Neu -> in Starface erstellen var match = FindMatch(oc, starfaceContacts);
Log($" Erstelle in Starface: {oc.DisplayName}"); if (match != null && !string.IsNullOrEmpty(match.StarfaceId))
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
{ {
newMappings.Add(new SyncMapping if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook))
{ {
ProfileId = profile.Id, _profileManager.AddOrUpdateMapping(new SyncMapping
OutlookEntryId = oc.OutlookEntryId, {
StarfaceId = created.StarfaceId, ProfileId = profile.Id,
LastSyncHash = oc.GetHash() OutlookEntryId = oc.OutlookEntryId,
}); StarfaceId = match.StarfaceId,
result.Created++; LastSyncHash = oc.GetHash()
Log($" Erstellt (OL->SF): {oc.DisplayName}"); });
result.Updated++;
}
} }
else else
{ {
Log($" FEHLER: Kontakt konnte nicht erstellt werden: {oc.DisplayName}"); var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
result.Errors++; if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
{
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId,
LastSyncHash = oc.GetHash()
});
result.Created++;
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Errors++; result.Errors++;
result.ErrorMessages.Add($"OL->SF {oc.DisplayName}: {ex.Message}"); result.ErrorMessages.Add($"{oc.DisplayName}: {ex.Message}");
} }
} }
} }
// ============================================ // Starface -> Outlook
// Phase 3: Neue Starface-Kontakte (ohne Mapping) if (profile.SyncDirection == SyncDirection.Both ||
// ============================================ profile.SyncDirection == SyncDirection.StarfaceToOutlook)
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)
{ {
var unmappedStarface = starfaceContacts Log("Synchronisiere Starface -> Outlook...");
.Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId)) foreach (var sc in starfaceContacts)
.ToList();
if (unmappedStarface.Count > 0)
Log($"Neue Starface-Kontakte: {unmappedStarface.Count}");
// Outlook-Kontakte die noch kein Mapping haben (fuer Duplikat-Check)
var unmappedOutlook = outlookContacts
.Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId))
.ToList();
foreach (var sc in unmappedStarface)
{ {
try try
{ {
// Duplikat-Check: existiert der Kontakt schon in Outlook? SyncMapping existing = null;
var match = FindMatch(sc, unmappedOutlook); if (!string.IsNullOrEmpty(sc.StarfaceId))
if (match != null) mappingByStarface.TryGetValue(sc.StarfaceId, out existing);
if (existing != null)
{ {
// Existiert schon -> verknuepfen und updaten var hash = sc.GetHash();
if (_outlookService.UpdateContact(match.OutlookEntryId, sc)) if (hash != existing.LastSyncHash)
{ {
newMappings.Add(new SyncMapping if (_outlookService.UpdateContact(existing.OutlookEntryId, sc))
{ {
ProfileId = profile.Id, existing.LastSyncHash = hash;
OutlookEntryId = match.OutlookEntryId, result.Updated++;
StarfaceId = sc.StarfaceId, }
LastSyncHash = sc.GetHash()
});
processedOutlookIds.Add(match.OutlookEntryId);
unmappedOutlook.Remove(match);
result.Updated++;
Log($" Verknuepft (SF->OL): {sc.DisplayName}");
} }
} }
else else
{ {
// Neu -> in Outlook erstellen var match = FindMatch(sc, outlookContacts);
var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath); if (match != null && !string.IsNullOrEmpty(match.OutlookEntryId))
if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId))
{ {
newMappings.Add(new SyncMapping if (_outlookService.UpdateContact(match.OutlookEntryId, sc))
{ {
ProfileId = profile.Id, _profileManager.AddOrUpdateMapping(new SyncMapping
OutlookEntryId = created.OutlookEntryId, {
StarfaceId = sc.StarfaceId, ProfileId = profile.Id,
LastSyncHash = sc.GetHash() OutlookEntryId = match.OutlookEntryId,
}); StarfaceId = sc.StarfaceId,
result.Created++; LastSyncHash = sc.GetHash()
Log($" Erstellt (SF->OL): {sc.DisplayName}"); });
result.Updated++;
}
}
else
{
var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath);
if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId))
{
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = created.OutlookEntryId,
StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
result.Created++;
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Errors++; result.Errors++;
result.ErrorMessages.Add($"SF->OL {sc.DisplayName}: {ex.Message}"); result.ErrorMessages.Add($"{sc.DisplayName}: {ex.Message}");
} }
} }
} }
// Mappings speichern
_profileManager.SaveMappings(profile.Id, newMappings);
_profileManager.UpdateLastSync(profile.Id); _profileManager.UpdateLastSync(profile.Id);
_profileManager.SaveMappings(profile.Id, mappings);
await starface.LogoutAsync(); await starface.LogoutAsync();
Log("Synchronisation abgeschlossen!");
Log($"Fertig: {result.Created} erstellt, {result.Updated} aktualisiert, {result.Errors} Fehler");
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@ -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.23</Version> <Version>0.0.0.7</Version>
<AssemblyVersion>0.0.0.23</AssemblyVersion> <AssemblyVersion>0.0.0.7</AssemblyVersion>
<FileVersion>0.0.0.23</FileVersion> <FileVersion>0.0.0.7</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>

View File

@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.0.23", Text = "Version 0.0.0.7",
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

View File

@ -1,108 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace StarfaceOutlookSync.UI
{
/// <summary>
/// Generiert das App-Icon programmatisch (Kontakt-Symbol mit Sync-Pfeilen).
/// Kein externes ICO-File noetig.
/// </summary>
public static class AppIcon
{
private static Icon _icon;
public static Icon GetIcon()
{
if (_icon != null) return _icon;
_icon = CreateIcon(32);
return _icon;
}
public static Icon GetSmallIcon()
{
return CreateIcon(16);
}
private static Icon CreateIcon(int size)
{
using (var bmp = new Bitmap(size, size))
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Transparent);
if (size >= 32)
DrawIcon32(g);
else
DrawIcon16(g);
return Icon.FromHandle(bmp.GetHicon());
}
}
private static void DrawIcon32(Graphics g)
{
// Hintergrund: abgerundetes Quadrat
using (var bgBrush = new SolidBrush(Color.FromArgb(0, 120, 212)))
{
FillRoundedRect(g, bgBrush, 0, 0, 31, 31, 6);
}
// Person (Kopf)
using (var whiteBrush = new SolidBrush(Color.White))
{
g.FillEllipse(whiteBrush, 11, 4, 10, 10);
// Person (Koerper)
g.FillPie(whiteBrush, 6, 14, 20, 18, 180, 180);
}
// Sync-Pfeile unten rechts
using (var arrowPen = new Pen(Color.FromArgb(120, 255, 120), 2f))
{
arrowPen.StartCap = LineCap.Round;
arrowPen.EndCap = LineCap.ArrowAnchor;
// Pfeil rechts
g.DrawArc(arrowPen, 20, 22, 10, 8, 200, 140);
arrowPen.Color = Color.FromArgb(255, 200, 80);
// Pfeil links
g.DrawArc(arrowPen, 20, 22, 10, 8, 20, 140);
}
}
private static void DrawIcon16(Graphics g)
{
// Hintergrund
using (var bgBrush = new SolidBrush(Color.FromArgb(0, 120, 212)))
{
FillRoundedRect(g, bgBrush, 0, 0, 15, 15, 3);
}
// Person (vereinfacht)
using (var whiteBrush = new SolidBrush(Color.White))
{
g.FillEllipse(whiteBrush, 4, 1, 7, 7);
g.FillPie(whiteBrush, 2, 8, 12, 10, 180, 180);
}
// Kleiner Sync-Indikator
using (var dotBrush = new SolidBrush(Color.FromArgb(120, 255, 120)))
{
g.FillEllipse(dotBrush, 11, 11, 4, 4);
}
}
private static void FillRoundedRect(Graphics g, Brush brush, int x, int y, int w, int h, int r)
{
using (var path = new GraphicsPath())
{
path.AddArc(x, y, r * 2, r * 2, 180, 90);
path.AddArc(x + w - r * 2, y, r * 2, r * 2, 270, 90);
path.AddArc(x + w - r * 2, y + h - r * 2, r * 2, r * 2, 0, 90);
path.AddArc(x, y + h - r * 2, r * 2, r * 2, 90, 90);
path.CloseFigure();
g.FillPath(brush, path);
}
}
}
}

View File

@ -18,11 +18,10 @@ 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, _btnInfo; private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo;
private StatusStrip _statusBar; private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel; private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer; private Timer _autoSyncTimer;
private volatile bool _syncRunning = false;
public MainForm() public MainForm()
{ {
@ -31,31 +30,14 @@ namespace StarfaceOutlookSync.UI
SetupAutoSync(); SetupAutoSync();
RefreshProfileList(); RefreshProfileList();
// Einstellungen laden und anwenden // Minimiert starten falls in Einstellungen aktiviert
var settings = UserSettings.Load(); var settings = UserSettings.Load();
settings.ApplyOutlookSecuritySetting();
if (settings.StartMinimized) if (settings.StartMinimized)
{ {
WindowState = FormWindowState.Minimized; WindowState = FormWindowState.Minimized;
ShowInTaskbar = false; ShowInTaskbar = false;
Visible = false; Visible = false;
} }
// Beim Start automatisch synchronisieren
if (settings.SyncOnStart)
{
_ = SyncAllProfiles();
}
}
private async Task SyncAllProfiles()
{
var profiles = _profileManager.GetProfiles().Where(p => p.Enabled).ToList();
foreach (var profile in profiles)
{
await RunSync(profile);
}
} }
protected override void SetVisibleCore(bool value) protected override void SetVisibleCore(bool value)
@ -76,11 +58,10 @@ 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(620, 450);
MinimumSize = new Size(830, 350); MinimumSize = new Size(500, 350);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9); Font = new Font("Segoe UI", 9);
Icon = AppIcon.GetIcon();
// Profil-Liste // Profil-Liste
_profileList = new ListView _profileList = new ListView
@ -116,19 +97,16 @@ namespace StarfaceOutlookSync.UI
_btnDelete = new Button { Text = "Loeschen", Width = 80, Height = 30 }; _btnDelete = new Button { Text = "Loeschen", Width = 80, Height = 30 };
_btnDelete.Click += (s, e) => DeleteProfile(); _btnDelete.Click += (s, e) => DeleteProfile();
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 }; _btnSync = new Button { Text = "Jetzt synchronisieren", Width = 150, Height = 30 };
_btnSync.Click += async (s, e) => await SyncSelectedProfile(); _btnSync.Click += async (s, e) => await SyncSelectedProfile();
_btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 }; _btnSettings = new Button { Text = "Einstellungen", Width = 100, Height = 30 };
_btnReset.Click += (s, e) => ResetSync();
_btnSettings = new Button { Text = "Einstellungen", Width = 95, Height = 30 };
_btnSettings.Click += (s, e) => ShowSettings(); _btnSettings.Click += (s, e) => ShowSettings();
_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, _btnInfo }); buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo });
// Statusbar // Statusbar
_statusBar = new StatusStrip(); _statusBar = new StatusStrip();
@ -143,26 +121,10 @@ namespace StarfaceOutlookSync.UI
private void SetupTrayIcon() private void SetupTrayIcon()
{ {
_trayMenu = new ContextMenuStrip(); _trayMenu = new ContextMenuStrip();
_trayIcon = new NotifyIcon
{
Text = "Starface Kontakt-Sync",
Icon = AppIcon.GetSmallIcon(),
ContextMenuStrip = _trayMenu,
Visible = true
};
_trayIcon.DoubleClick += (s, e) => ShowMainWindow();
UpdateTrayMenu();
}
private void UpdateTrayMenu()
{
_trayMenu.Items.Clear();
_trayMenu.Items.Add("Oeffnen", null, (s, e) => ShowMainWindow()); _trayMenu.Items.Add("Oeffnen", null, (s, e) => ShowMainWindow());
_trayMenu.Items.Add("-"); _trayMenu.Items.Add("-");
// Schnell-Sync fuer jedes Profil
var profiles = _profileManager.GetProfiles(); var profiles = _profileManager.GetProfiles();
foreach (var p in profiles.Where(p => p.Enabled)) foreach (var p in profiles.Where(p => p.Enabled))
{ {
@ -176,8 +138,17 @@ namespace StarfaceOutlookSync.UI
if (profiles.Any(p => p.Enabled)) if (profiles.Any(p => p.Enabled))
_trayMenu.Items.Add("-"); _trayMenu.Items.Add("-");
_trayMenu.Items.Add("Ueber", null, (s, e) => ShowAbout());
_trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication()); _trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication());
_trayIcon = new NotifyIcon
{
Text = "Starface Kontakt-Sync",
Icon = SystemIcons.Application, // Placeholder, wird durch eigenes Icon ersetzt
ContextMenuStrip = _trayMenu,
Visible = true
};
_trayIcon.DoubleClick += (s, e) => ShowMainWindow();
} }
private void SetupAutoSync() private void SetupAutoSync()
@ -245,7 +216,7 @@ namespace StarfaceOutlookSync.UI
} }
// Tray-Menu aktualisieren // Tray-Menu aktualisieren
UpdateTrayMenu(); SetupTrayIcon();
} }
private void NewProfile() private void NewProfile()
@ -284,36 +255,6 @@ namespace StarfaceOutlookSync.UI
} }
} }
private void ResetSync()
{
if (_profileList.SelectedItems.Count == 0)
{
MessageBox.Show("Bitte ein Profil auswaehlen.", "Sync Reset",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var profile = _profileList.SelectedItems[0].Tag as SyncProfile;
if (profile == null) return;
var msg = $"Sync-Zuordnungen fuer '{profile.Name}' zuruecksetzen?\n\n" +
"Alle Kontakt-Verknuepfungen werden geloescht.\n" +
"Beim naechsten Sync werden die Kontakte neu abgeglichen.\n" +
"Es werden keine Kontakte geloescht.";
if (MessageBox.Show(msg, "Sync Reset",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
{
_profileManager.SaveMappings(profile.Id, new List<SyncMapping>());
// LastSync auch zuruecksetzen
profile.LastSync = "";
_profileManager.UpdateProfile(profile);
RefreshProfileList();
MessageBox.Show("Sync-Zuordnungen wurden zurueckgesetzt.",
"Sync Reset", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private Task SyncSelectedProfile() private Task SyncSelectedProfile()
{ {
if (_profileList.SelectedItems.Count == 0) if (_profileList.SelectedItems.Count == 0)
@ -337,13 +278,6 @@ namespace StarfaceOutlookSync.UI
private async Task RunSync(SyncProfile profile) private async Task RunSync(SyncProfile profile)
{ {
if (_syncRunning)
{
SetStatus("Sync laeuft bereits, bitte warten...");
return;
}
_syncRunning = true;
try try
{ {
SetStatus($"Synchronisiere '{profile.Name}'..."); SetStatus($"Synchronisiere '{profile.Name}'...");
@ -366,10 +300,6 @@ namespace StarfaceOutlookSync.UI
ex.Message, ToolTipIcon.Error); ex.Message, ToolTipIcon.Error);
SetStatus($"Fehler: {ex.Message}"); SetStatus($"Fehler: {ex.Message}");
} }
finally
{
_syncRunning = false;
}
} }
private void SetStatus(string text) private void SetStatus(string text)

View File

@ -333,23 +333,9 @@ namespace StarfaceOutlookSync.UI
}; };
if (_isNew) if (_isNew)
{
_pm.AddProfile(profile); _pm.AddProfile(profile);
}
else else
{
// Wenn Adressbuch gewechselt wurde, Mappings zuruecksetzen
if (_existingProfile.StarfaceAddressBook?.TagId != profile.StarfaceAddressBook?.TagId
|| _existingProfile.StarfaceAddressBook?.Type != profile.StarfaceAddressBook?.Type)
{
_pm.SaveMappings(profile.Id, new List<SyncMapping>());
profile.LastSync = "";
MessageBox.Show(
"Adressbuch wurde geaendert.\nSync-Zuordnungen wurden automatisch zurueckgesetzt.",
"Adressbuch geaendert", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
_pm.UpdateProfile(profile); _pm.UpdateProfile(profile);
}
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();

View File

@ -6,7 +6,7 @@ namespace StarfaceOutlookSync.UI
{ {
public class SettingsForm : Form public class SettingsForm : Form
{ {
private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook; private CheckBox _chkStartMinimized;
private Button _btnSave, _btnCancel; private Button _btnSave, _btnCancel;
private readonly UserSettings _settings; private readonly UserSettings _settings;
@ -19,7 +19,7 @@ namespace StarfaceOutlookSync.UI
private void InitializeComponent() private void InitializeComponent()
{ {
Text = "Einstellungen"; Text = "Einstellungen";
Size = new Size(380, 250); Size = new Size(350, 180);
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false; MaximizeBox = false;
MinimizeBox = false; MinimizeBox = false;
@ -33,47 +33,20 @@ namespace StarfaceOutlookSync.UI
Checked = _settings.StartMinimized Checked = _settings.StartMinimized
}; };
_chkSyncOnStart = new CheckBox
{
Text = "Beim Start automatisch synchronisieren",
Left = 20, Top = 52, AutoSize = true,
Checked = _settings.SyncOnStart
};
_chkAutoAcceptOutlook = new CheckBox
{
Text = "Outlook-Sicherheitsabfrage automatisch erlauben",
Left = 20, Top = 80, AutoSize = true,
Checked = _settings.AutoAcceptOutlookPrompt
};
var hintText = "Hinweis: Outlook muss nach Aenderung neu gestartet werden.";
if (UserSettings.IsOutlookSecurityLockedByPolicy())
hintText += "\nAuf Domaenen-PCs: App einmalig als Admin starten!";
var lblHint = new Label
{
Text = hintText,
Left = 38, Top = 102, Width = 310, Height = 36,
ForeColor = UserSettings.IsOutlookSecurityLockedByPolicy() ? Color.OrangeRed : Color.Gray,
Font = new Font("Segoe UI", 8)
};
_btnSave = new Button _btnSave = new Button
{ {
Text = "Speichern", Left = 95, Top = 170, Width = 85, Height = 28, Text = "Speichern", Left = 80, Top = 100, Width = 85, Height = 28,
DialogResult = DialogResult.None DialogResult = DialogResult.None
}; };
_btnSave.Click += (s, e) => Save(); _btnSave.Click += (s, e) => Save();
_btnCancel = new Button _btnCancel = new Button
{ {
Text = "Abbrechen", Left = 189, Top = 170, Width = 85, Height = 28, Text = "Abbrechen", Left = 174, Top = 100, Width = 85, Height = 28,
DialogResult = DialogResult.Cancel DialogResult = DialogResult.Cancel
}; };
Size = new Size(380, 260); Controls.AddRange(new Control[] { _chkStartMinimized, _btnSave, _btnCancel });
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint, _btnSave, _btnCancel });
AcceptButton = _btnSave; AcceptButton = _btnSave;
CancelButton = _btnCancel; CancelButton = _btnCancel;
} }
@ -81,8 +54,6 @@ namespace StarfaceOutlookSync.UI
private void Save() private void Save()
{ {
_settings.StartMinimized = _chkStartMinimized.Checked; _settings.StartMinimized = _chkStartMinimized.Checked;
_settings.SyncOnStart = _chkSyncOnStart.Checked;
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
_settings.Save(); _settings.Save();
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();

View File

@ -41,8 +41,7 @@ namespace StarfaceOutlookSync.UI
_progressBar = new ProgressBar _progressBar = new ProgressBar
{ {
Left = 12, Top = 38, Width = 460, Height = 22, Left = 12, Top = 38, Width = 460, Height = 22,
Style = ProgressBarStyle.Blocks, Style = ProgressBarStyle.Marquee
Value = 0
}; };
_txtLog = new TextBox _txtLog = new TextBox