Compare commits

..

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

13 changed files with 335 additions and 1207 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.4"
#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

@ -2,75 +2,44 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using StarfaceOutlookSync.Models; using StarfaceOutlookSync.Models;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace StarfaceOutlookSync.Services namespace StarfaceOutlookSync.Services
{ {
/// <summary>
/// Zugriff auf Outlook-Kontakte per dynamic COM.
/// Funktioniert mit jeder Outlook Classic Version (2013-2024)
/// ohne Abhaengigkeit von einer bestimmten Interop-DLL.
/// </summary>
public class OutlookContactsService : IDisposable public class OutlookContactsService : IDisposable
{ {
private dynamic _outlookApp; private Outlook.Application _outlookApp;
private bool _weStartedOutlook; private bool _weStartedOutlook;
// OlDefaultFolders.olFolderContacts = 10 // Marshal.GetActiveObject existiert nicht in .NET 8, daher P/Invoke
private const int OlFolderContacts = 10;
// OlItemType.olContactItem = 2
private const int OlContactItem = 2;
[DllImport("oleaut32.dll", PreserveSig = false)] [DllImport("oleaut32.dll", PreserveSig = false)]
private static extern void GetActiveObject( private static extern void GetActiveObject([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
[MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
IntPtr pvReserved,
[MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
private static object GetActiveComObject(string progId) private static object GetActiveComObject(string progId)
{ {
var type = Type.GetTypeFromProgID(progId, false); var clsid = Type.GetTypeFromProgID(progId, true).GUID;
if (type == null) return null; GetActiveObject(clsid, IntPtr.Zero, out var obj);
GetActiveObject(type.GUID, IntPtr.Zero, out var obj);
return obj; return obj;
} }
private dynamic GetOutlookApp() private Outlook.Application GetOutlookApp()
{ {
if (_outlookApp != null) return _outlookApp; if (_outlookApp != null) return _outlookApp;
// Versuch 1: Laufende Outlook-Instanz finden
try try
{ {
var obj = GetActiveComObject("Outlook.Application"); // Versuche laufende Outlook-Instanz zu finden
if (obj != null) _outlookApp = (Outlook.Application)GetActiveComObject("Outlook.Application");
{ _weStartedOutlook = false;
_outlookApp = obj;
_weStartedOutlook = false;
return _outlookApp;
}
} }
catch { } catch
// Versuch 2: Outlook per COM starten
try
{ {
var outlookType = Type.GetTypeFromProgID("Outlook.Application", false); // Outlook starten falls nicht laufend
if (outlookType != null) _outlookApp = new Outlook.Application();
{ _weStartedOutlook = true;
_outlookApp = Activator.CreateInstance(outlookType);
_weStartedOutlook = true;
return _outlookApp;
}
} }
catch { }
throw new InvalidOperationException( return _outlookApp;
"Outlook Classic konnte nicht gefunden werden.\n\n" +
"Moegliche Ursachen:\n" +
"- Outlook Classic ist nicht installiert\n" +
"- Outlook Classic ist nicht gestartet\n" +
"- Das neue Outlook wird verwendet (wird noch nicht unterstuetzt)\n\n" +
"Bitte Outlook Classic starten und erneut versuchen.");
} }
public List<string> GetContactFolderPaths() public List<string> GetContactFolderPaths()
@ -81,235 +50,128 @@ namespace StarfaceOutlookSync.Services
var app = GetOutlookApp(); var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI"); var ns = app.GetNamespace("MAPI");
// Alle Stores durchgehen // Standard-Kontaktordner
var stores = ns.Stores; var defaultFolder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
for (int i = 1; i <= (int)stores.Count; i++) folders.Add(defaultFolder.FolderPath);
{
try
{
var store = stores[i];
var rootFolder = store.GetRootFolder();
FindContactFoldersRecursive(rootFolder, folders);
Marshal.ReleaseComObject(rootFolder);
Marshal.ReleaseComObject(store);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scanning store {i}: {ex.Message}");
}
}
// Falls nichts gefunden, Standard-Kontaktordner als Fallback // Unterordner
if (folders.Count == 0) AddSubFolders(defaultFolder, folders);
// Weitere Kontaktordner in anderen Stores
foreach (Outlook.Store store in ns.Stores)
{ {
try try
{ {
var defaultFolder = ns.GetDefaultFolder(OlFolderContacts); var rootFolder = store.GetRootFolder();
folders.Add((string)defaultFolder.FolderPath); FindContactFolders(rootFolder, folders);
Marshal.ReleaseComObject(defaultFolder);
} }
catch { } catch { }
} }
Marshal.ReleaseComObject(stores);
Marshal.ReleaseComObject(ns); Marshal.ReleaseComObject(ns);
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Error getting folders: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error getting folders: {ex.Message}");
throw;
} }
return folders; return folders;
} }
private void FindContactFoldersRecursive(dynamic folder, List<string> paths) private void AddSubFolders(Outlook.MAPIFolder folder, List<string> paths)
{ {
try foreach (Outlook.MAPIFolder sub in folder.Folders)
{ {
if ((int)folder.DefaultItemType == OlContactItem) if (sub.DefaultItemType == Outlook.OlItemType.olContactItem)
{ {
string path = folder.FolderPath; if (!paths.Contains(sub.FolderPath))
if (!paths.Contains(path)) paths.Add(sub.FolderPath);
paths.Add(path); AddSubFolders(sub, paths);
} }
var subFolders = folder.Folders;
for (int i = 1; i <= (int)subFolders.Count; i++)
{
try
{
var sub = subFolders[i];
FindContactFoldersRecursive(sub, paths);
Marshal.ReleaseComObject(sub);
}
catch { }
}
Marshal.ReleaseComObject(subFolders);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scanning folder: {ex.Message}");
} }
} }
private dynamic FindFolderByPath(dynamic folder, string targetPath) private void FindContactFolders(Outlook.MAPIFolder folder, List<string> paths)
{ {
try if (folder.DefaultItemType == Outlook.OlItemType.olContactItem)
{ {
string currentPath = folder.FolderPath; if (!paths.Contains(folder.FolderPath))
if (currentPath == targetPath) paths.Add(folder.FolderPath);
return folder; }
var subs = folder.Folders; foreach (Outlook.MAPIFolder sub in folder.Folders)
for (int i = 1; i <= (int)subs.Count; i++) {
{ FindContactFolders(sub, paths);
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 Outlook.MAPIFolder GetFolderByPath(string folderPath)
{ {
var app = GetOutlookApp(); var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI"); var ns = app.GetNamespace("MAPI");
// Standard-Kontaktordner als Fallback
if (string.IsNullOrEmpty(folderPath)) if (string.IsNullOrEmpty(folderPath))
return ns.GetDefaultFolder(OlFolderContacts); return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
try try
{ {
// Versuch 1: Alle Kontaktordner durchsuchen und per FolderPath matchen // Pfad durchlaufen
var stores = ns.Stores;
for (int i = 1; i <= (int)stores.Count; i++)
{
try
{
var store = stores[i];
var root = store.GetRootFolder();
var match = FindFolderByPath(root, folderPath);
if (match != null)
{
Marshal.ReleaseComObject(stores);
return match;
}
Marshal.ReleaseComObject(root);
Marshal.ReleaseComObject(store);
}
catch { }
}
Marshal.ReleaseComObject(stores);
// Versuch 2: Namespace.Folders direkt navigieren
var parts = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); var parts = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 1) Outlook.MAPIFolder current = null;
foreach (Outlook.Store store in ns.Stores)
{ {
var topFolders = ns.Folders; if (store.GetRootFolder().Name == parts[0] ||
for (int i = 1; i <= (int)topFolders.Count; i++) store.GetRootFolder().FolderPath.TrimStart('\\') == parts[0])
{ {
var topFolder = topFolders[i]; current = store.GetRootFolder();
if ((string)topFolder.Name == parts[0]) break;
{
dynamic current = topFolder;
for (int p = 1; p < parts.Length; p++)
{
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(topFolders);
} }
System.Diagnostics.Debug.WriteLine($"Folder not found by path: {folderPath}, using default"); if (current == null)
return ns.GetDefaultFolder(OlFolderContacts); return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
for (int i = 1; i < parts.Length; i++)
{
bool found = false;
foreach (Outlook.MAPIFolder sub in current.Folders)
{
if (sub.Name == parts[i])
{
current = sub;
found = true;
break;
}
}
if (!found)
return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
}
return current;
} }
catch catch
{ {
return ns.GetDefaultFolder(OlFolderContacts); return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
} }
} }
public List<UnifiedContact> GetContacts(string folderPath) public List<UnifiedContact> GetContacts(string folderPath)
{ {
var contacts = new List<UnifiedContact>(); var contacts = new List<UnifiedContact>();
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++) foreach (var item in items)
{ {
dynamic item = null; if (item is Outlook.ContactItem ci)
try
{ {
item = items[i]; contacts.Add(MapFromOutlook(ci));
int itemClass = -1; Marshal.ReleaseComObject(ci);
try { itemClass = (int)item.Class; } catch { }
// olContact = 40, aber manche Kontakte melden Class anders
// Versuch einfach die Kontakt-Felder zu lesen
if (itemClass == 40)
{
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}");
}
finally
{
if (item != null) try { Marshal.ReleaseComObject(item); } catch { }
} }
} }
@ -329,8 +191,7 @@ namespace StarfaceOutlookSync.Services
try try
{ {
var folder = GetFolderByPath(folderPath); var folder = GetFolderByPath(folderPath);
var items = folder.Items; var ci = (Outlook.ContactItem)folder.Items.Add(Outlook.OlItemType.olContactItem);
dynamic ci = items.Add(OlContactItem);
MapToOutlook(contact, ci); MapToOutlook(contact, ci);
ci.Save(); ci.Save();
@ -338,7 +199,6 @@ namespace StarfaceOutlookSync.Services
contact.OutlookEntryId = ci.EntryID; contact.OutlookEntryId = ci.EntryID;
Marshal.ReleaseComObject(ci); Marshal.ReleaseComObject(ci);
Marshal.ReleaseComObject(items);
Marshal.ReleaseComObject(folder); Marshal.ReleaseComObject(folder);
return contact; return contact;
@ -356,7 +216,7 @@ namespace StarfaceOutlookSync.Services
{ {
var app = GetOutlookApp(); var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI"); var ns = app.GetNamespace("MAPI");
dynamic ci = ns.GetItemFromID(entryId); var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId);
MapToOutlook(contact, ci); MapToOutlook(contact, ci);
ci.Save(); ci.Save();
@ -379,7 +239,7 @@ namespace StarfaceOutlookSync.Services
{ {
var app = GetOutlookApp(); var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI"); var ns = app.GetNamespace("MAPI");
dynamic ci = ns.GetItemFromID(entryId); var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId);
ci.Delete(); ci.Delete();
Marshal.ReleaseComObject(ci); Marshal.ReleaseComObject(ci);
@ -394,58 +254,36 @@ namespace StarfaceOutlookSync.Services
} }
} }
private static string SafeGet(dynamic ci, string property) private UnifiedContact MapFromOutlook(Outlook.ContactItem ci)
{
try
{
object val = ci.GetType().InvokeMember(property,
System.Reflection.BindingFlags.GetProperty, null, ci, null);
return val?.ToString() ?? "";
}
catch { return ""; }
}
private UnifiedContact MapFromOutlook(dynamic ci)
{ {
return new UnifiedContact return new UnifiedContact
{ {
OutlookEntryId = SafeGet(ci, "EntryID"), OutlookEntryId = ci.EntryID ?? "",
FirstName = SafeGet(ci, "FirstName"), FirstName = ci.FirstName ?? "",
LastName = SafeGet(ci, "LastName"), LastName = ci.LastName ?? "",
Company = SafeGet(ci, "CompanyName"), Company = ci.CompanyName ?? "",
JobTitle = SafeGet(ci, "JobTitle"), JobTitle = ci.JobTitle ?? "",
Email = SafeGet(ci, "Email1Address"), Email = ci.Email1Address ?? "",
EmailSecondary = SafeGet(ci, "Email2Address"), EmailSecondary = ci.Email2Address ?? "",
PhoneWork = SafeGet(ci, "BusinessTelephoneNumber"), PhoneWork = ci.BusinessTelephoneNumber ?? "",
PhoneMobile = SafeGet(ci, "MobileTelephoneNumber"), PhoneMobile = ci.MobileTelephoneNumber ?? "",
PhoneHome = SafeGet(ci, "HomeTelephoneNumber"), PhoneHome = ci.HomeTelephoneNumber ?? "",
Fax = SafeGet(ci, "BusinessFaxNumber"), Fax = ci.BusinessFaxNumber ?? "",
Street = SafeGet(ci, "BusinessAddressStreet"), Street = ci.BusinessAddressStreet ?? "",
City = SafeGet(ci, "BusinessAddressCity"), City = ci.BusinessAddressCity ?? "",
PostalCode = SafeGet(ci, "BusinessAddressPostalCode"), PostalCode = ci.BusinessAddressPostalCode ?? "",
State = SafeGet(ci, "BusinessAddressState"), State = ci.BusinessAddressState ?? "",
Country = SafeGet(ci, "BusinessAddressCountry"), Country = ci.BusinessAddressCountry ?? "",
Website = SafeGet(ci, "WebPage"), Website = ci.WebPage ?? "",
Notes = SafeGet(ci, "Body"), Notes = ci.Body ?? "",
Salutation = SafeGet(ci, "Title"), Salutation = ci.Title ?? "",
Birthday = GetBirthdayString(ci) Title = ci.Suffix ?? "",
Birthday = ci.Birthday != DateTime.MinValue && ci.Birthday.Year > 1900
? ci.Birthday.ToString("yyyy-MM-dd") : ""
}; };
} }
private string GetBirthdayString(dynamic ci) private void MapToOutlook(UnifiedContact contact, Outlook.ContactItem ci)
{
try
{
object val = ci.GetType().InvokeMember("Birthday",
System.Reflection.BindingFlags.GetProperty, null, ci, null);
if (val is DateTime bday && bday.Year > 1900 && bday != DateTime.MinValue)
return bday.ToString("yyyy-MM-dd");
}
catch { }
return "";
}
private void MapToOutlook(UnifiedContact contact, dynamic ci)
{ {
ci.FirstName = contact.FirstName; ci.FirstName = contact.FirstName;
ci.LastName = contact.LastName; ci.LastName = contact.LastName;
@ -482,7 +320,7 @@ namespace StarfaceOutlookSync.Services
{ {
try { _outlookApp.Quit(); } catch { } try { _outlookApp.Quit(); } catch { }
} }
try { Marshal.ReleaseComObject(_outlookApp); } catch { } Marshal.ReleaseComObject(_outlookApp);
_outlookApp = null; _outlookApp = null;
} }
} }

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.4</Version>
<AssemblyVersion>0.0.0.23</AssemblyVersion> <AssemblyVersion>0.0.0.4</AssemblyVersion>
<FileVersion>0.0.0.23</FileVersion> <FileVersion>0.0.0.4</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>
@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Office.Interop.Outlook" Version="15.0.4797.1004" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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.4",
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

@ -142,12 +142,9 @@ namespace StarfaceOutlookSync.UI
_outlookFolderPaths = outlook.GetContactFolderPaths(); _outlookFolderPaths = outlook.GetContactFolderPaths();
} }
} }
catch (Exception ex) catch
{ {
MessageBox.Show( _outlookFolderPaths = new List<string> { "\\\\Kontakte" };
$"Outlook-Kontaktordner konnten nicht geladen werden:\n{ex.Message}\n\nIst Outlook gestartet?",
"Outlook-Verbindung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_outlookFolderPaths = new List<string>();
} }
// Bestehende Werte laden // Bestehende Werte laden
@ -333,23 +330,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