Compare commits

..

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

14 changed files with 354 additions and 1503 deletions

View File

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

View File

@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.23"
#define MyAppVersion "0.0.0.2"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"

View File

@ -1,120 +0,0 @@
using System;
using System.IO;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace StarfaceOutlookSync.Models
{
public class UserSettings
{
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(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StarfaceOutlookSync", "settings.json");
public static UserSettings Load()
{
try
{
if (File.Exists(SettingsFile))
return JsonConvert.DeserializeObject<UserSettings>(File.ReadAllText(SettingsFile))
?? new UserSettings();
}
catch { }
return new UserSettings();
}
public void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(SettingsFile));
File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented));
}
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.Runtime.InteropServices;
using StarfaceOutlookSync.Models;
using Outlook = Microsoft.Office.Interop.Outlook;
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
{
private dynamic _outlookApp;
private Outlook.Application _outlookApp;
private bool _weStartedOutlook;
// OlDefaultFolders.olFolderContacts = 10
private const int OlFolderContacts = 10;
// OlItemType.olContactItem = 2
private const int OlContactItem = 2;
// Marshal.GetActiveObject existiert nicht in .NET 8, daher P/Invoke
[DllImport("oleaut32.dll", PreserveSig = false)]
private static extern void GetActiveObject(
[MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
IntPtr pvReserved,
[MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
private static extern void GetActiveObject([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
private static object GetActiveComObject(string progId)
{
var type = Type.GetTypeFromProgID(progId, false);
if (type == null) return null;
GetActiveObject(type.GUID, IntPtr.Zero, out var obj);
var clsid = Type.GetTypeFromProgID(progId, true).GUID;
GetActiveObject(clsid, IntPtr.Zero, out var obj);
return obj;
}
private dynamic GetOutlookApp()
private Outlook.Application GetOutlookApp()
{
if (_outlookApp != null) return _outlookApp;
// Versuch 1: Laufende Outlook-Instanz finden
try
{
var obj = GetActiveComObject("Outlook.Application");
if (obj != null)
{
_outlookApp = obj;
_weStartedOutlook = false;
return _outlookApp;
}
// Versuche laufende Outlook-Instanz zu finden
_outlookApp = (Outlook.Application)GetActiveComObject("Outlook.Application");
_weStartedOutlook = false;
}
catch { }
// Versuch 2: Outlook per COM starten
try
catch
{
var outlookType = Type.GetTypeFromProgID("Outlook.Application", false);
if (outlookType != null)
{
_outlookApp = Activator.CreateInstance(outlookType);
_weStartedOutlook = true;
return _outlookApp;
}
// Outlook starten falls nicht laufend
_outlookApp = new Outlook.Application();
_weStartedOutlook = true;
}
catch { }
throw new InvalidOperationException(
"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.");
return _outlookApp;
}
public List<string> GetContactFolderPaths()
@ -81,235 +50,128 @@ namespace StarfaceOutlookSync.Services
var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI");
// Alle Stores durchgehen
var stores = ns.Stores;
for (int i = 1; i <= (int)stores.Count; i++)
{
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}");
}
}
// Standard-Kontaktordner
var defaultFolder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
folders.Add(defaultFolder.FolderPath);
// Falls nichts gefunden, Standard-Kontaktordner als Fallback
if (folders.Count == 0)
// Unterordner
AddSubFolders(defaultFolder, folders);
// Weitere Kontaktordner in anderen Stores
foreach (Outlook.Store store in ns.Stores)
{
try
{
var defaultFolder = ns.GetDefaultFolder(OlFolderContacts);
folders.Add((string)defaultFolder.FolderPath);
Marshal.ReleaseComObject(defaultFolder);
var rootFolder = store.GetRootFolder();
FindContactFolders(rootFolder, folders);
}
catch { }
}
Marshal.ReleaseComObject(stores);
Marshal.ReleaseComObject(ns);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting folders: {ex.Message}");
throw;
}
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(path))
paths.Add(path);
if (!paths.Contains(sub.FolderPath))
paths.Add(sub.FolderPath);
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 (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);
if (!paths.Contains(folder.FolderPath))
paths.Add(folder.FolderPath);
}
foreach (Outlook.MAPIFolder sub in folder.Folders)
{
FindContactFolders(sub, paths);
}
catch { }
return null;
}
private dynamic GetFolderByPath(string folderPath)
private Outlook.MAPIFolder GetFolderByPath(string folderPath)
{
var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI");
// Standard-Kontaktordner als Fallback
if (string.IsNullOrEmpty(folderPath))
return ns.GetDefaultFolder(OlFolderContacts);
return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
try
{
// Versuch 1: Alle Kontaktordner durchsuchen und per FolderPath matchen
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
// Pfad durchlaufen
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;
for (int i = 1; i <= (int)topFolders.Count; i++)
if (store.GetRootFolder().Name == parts[0] ||
store.GetRootFolder().FolderPath.TrimStart('\\') == parts[0])
{
var topFolder = topFolders[i];
if ((string)topFolder.Name == parts[0])
{
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);
current = store.GetRootFolder();
break;
}
Marshal.ReleaseComObject(topFolders);
}
System.Diagnostics.Debug.WriteLine($"Folder not found by path: {folderPath}, using default");
return ns.GetDefaultFolder(OlFolderContacts);
if (current == null)
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
{
return ns.GetDefaultFolder(OlFolderContacts);
return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
}
}
public List<UnifiedContact> GetContacts(string folderPath)
{
var contacts = new List<UnifiedContact>();
try
{
var folder = GetFolderByPath(folderPath);
System.Diagnostics.Debug.WriteLine($"Reading contacts from: {(string)folder.FolderPath}");
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;
try
if (item is Outlook.ContactItem ci)
{
item = items[i];
int itemClass = -1;
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 { }
contacts.Add(MapFromOutlook(ci));
Marshal.ReleaseComObject(ci);
}
}
@ -329,8 +191,7 @@ namespace StarfaceOutlookSync.Services
try
{
var folder = GetFolderByPath(folderPath);
var items = folder.Items;
dynamic ci = items.Add(OlContactItem);
var ci = (Outlook.ContactItem)folder.Items.Add(Outlook.OlItemType.olContactItem);
MapToOutlook(contact, ci);
ci.Save();
@ -338,7 +199,6 @@ namespace StarfaceOutlookSync.Services
contact.OutlookEntryId = ci.EntryID;
Marshal.ReleaseComObject(ci);
Marshal.ReleaseComObject(items);
Marshal.ReleaseComObject(folder);
return contact;
@ -356,7 +216,7 @@ namespace StarfaceOutlookSync.Services
{
var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI");
dynamic ci = ns.GetItemFromID(entryId);
var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId);
MapToOutlook(contact, ci);
ci.Save();
@ -379,7 +239,7 @@ namespace StarfaceOutlookSync.Services
{
var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI");
dynamic ci = ns.GetItemFromID(entryId);
var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId);
ci.Delete();
Marshal.ReleaseComObject(ci);
@ -394,58 +254,36 @@ 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(Outlook.ContactItem ci)
{
return new UnifiedContact
{
OutlookEntryId = SafeGet(ci, "EntryID"),
FirstName = SafeGet(ci, "FirstName"),
LastName = SafeGet(ci, "LastName"),
Company = SafeGet(ci, "CompanyName"),
JobTitle = SafeGet(ci, "JobTitle"),
Email = SafeGet(ci, "Email1Address"),
EmailSecondary = SafeGet(ci, "Email2Address"),
PhoneWork = SafeGet(ci, "BusinessTelephoneNumber"),
PhoneMobile = SafeGet(ci, "MobileTelephoneNumber"),
PhoneHome = SafeGet(ci, "HomeTelephoneNumber"),
Fax = SafeGet(ci, "BusinessFaxNumber"),
Street = SafeGet(ci, "BusinessAddressStreet"),
City = SafeGet(ci, "BusinessAddressCity"),
PostalCode = SafeGet(ci, "BusinessAddressPostalCode"),
State = SafeGet(ci, "BusinessAddressState"),
Country = SafeGet(ci, "BusinessAddressCountry"),
Website = SafeGet(ci, "WebPage"),
Notes = SafeGet(ci, "Body"),
Salutation = SafeGet(ci, "Title"),
Birthday = GetBirthdayString(ci)
OutlookEntryId = ci.EntryID ?? "",
FirstName = ci.FirstName ?? "",
LastName = ci.LastName ?? "",
Company = ci.CompanyName ?? "",
JobTitle = ci.JobTitle ?? "",
Email = ci.Email1Address ?? "",
EmailSecondary = ci.Email2Address ?? "",
PhoneWork = ci.BusinessTelephoneNumber ?? "",
PhoneMobile = ci.MobileTelephoneNumber ?? "",
PhoneHome = ci.HomeTelephoneNumber ?? "",
Fax = ci.BusinessFaxNumber ?? "",
Street = ci.BusinessAddressStreet ?? "",
City = ci.BusinessAddressCity ?? "",
PostalCode = ci.BusinessAddressPostalCode ?? "",
State = ci.BusinessAddressState ?? "",
Country = ci.BusinessAddressCountry ?? "",
Website = ci.WebPage ?? "",
Notes = ci.Body ?? "",
Salutation = ci.Title ?? "",
Title = ci.Suffix ?? "",
Birthday = ci.Birthday != DateTime.MinValue && ci.Birthday.Year > 1900
? ci.Birthday.ToString("yyyy-MM-dd") : ""
};
}
private string GetBirthdayString(dynamic 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)
private void MapToOutlook(UnifiedContact contact, Outlook.ContactItem ci)
{
ci.FirstName = contact.FirstName;
ci.LastName = contact.LastName;
@ -482,7 +320,7 @@ namespace StarfaceOutlookSync.Services
{
try { _outlookApp.Quit(); } catch { }
}
try { Marshal.ReleaseComObject(_outlookApp); } catch { }
Marshal.ReleaseComObject(_outlookApp);
_outlookApp = null;
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
@ -116,74 +115,51 @@ namespace StarfaceOutlookSync.Services
{
var books = new List<StarfaceAddressBook>();
// Alle Tags laden - die Starface nutzt Tags als Adressbuch-Zuordnung
var allTags = new JArray();
books.Add(new StarfaceAddressBook
{
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
{
var resp = await _http.GetAsync($"{_baseUrl}/contacts/tags");
if (resp.IsSuccessStatusCode)
{
allTags = JArray.Parse(await resp.Content.ReadAsStringAsync());
OnDebug?.Invoke($"Gefundene Tags: {allTags.Count}");
foreach (var t in allTags)
OnDebug?.Invoke($" Tag: {t["name"]} (id: {t["id"]}, alias: {t["alias"]}, owner: {t["owner"]})");
var tags = JArray.Parse(await resp.Content.ReadAsStringAsync());
foreach (var tag in tags)
{
books.Add(new StarfaceAddressBook
{
Type = "tag",
TagId = tag["id"]?.ToString() ?? "",
Name = $"Tag: {tag["name"]}"
});
}
}
}
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;
}
public event Action<string> OnDebug;
public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book)
{
var contacts = new List<UnifiedContact>();
int page = 0;
const int pageSize = 200;
bool firstPage = true;
while (true)
{
@ -196,68 +172,11 @@ namespace StarfaceOutlookSync.Services
var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}");
if (!resp.IsSuccessStatusCode) break;
var body = 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;
}
var array = JArray.Parse(await resp.Content.ReadAsStringAsync());
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)
{
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 { }
}
contacts.Add(MapFromStarface(item));
if (array.Count < pageSize) break;
page++;
@ -269,34 +188,15 @@ namespace StarfaceOutlookSync.Services
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
{
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 = "";
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}";
var body = sfContact.ToString();
OnDebug?.Invoke($"POST /contacts{query} Body:\n{body}");
var content = new StringContent(body, Encoding.UTF8, "application/json");
var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json");
var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content);
var respBody = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode) return null;
if (!resp.IsSuccessStatusCode)
{
OnDebug?.Invoke($"POST /contacts fehlgeschlagen: {(int)resp.StatusCode} {resp.StatusCode}\n{respBody}");
return null;
}
var created = JObject.Parse(respBody);
var created = JObject.Parse(await resp.Content.ReadAsStringAsync());
return MapFromStarface(created);
}
@ -304,27 +204,12 @@ namespace StarfaceOutlookSync.Services
{
var sfContact = MapToStarface(contact);
sfContact["id"] = contactId;
// Tag beibehalten
if (!string.IsNullOrEmpty(book.TagId))
{
sfContact["tags"] = new JArray { new JObject { ["id"] = book.TagId } };
}
var query = "";
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}";
var body = sfContact.ToString();
var content = new StringContent(body, Encoding.UTF8, "application/json");
var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json");
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;
}
@ -339,10 +224,7 @@ namespace StarfaceOutlookSync.Services
var contact = new UnifiedContact();
contact.StarfaceId = item["id"]?.ToString() ?? "";
// Attribute per "name"-Feld mappen (zuverlaessiger als displayKey,
// 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 attrs = new Dictionary<string, string>();
var blocks = item["blocks"] as JArray;
if (blocks != null)
{
@ -352,123 +234,79 @@ namespace StarfaceOutlookSync.Services
if (blockAttrs == null) continue;
foreach (var attr in blockAttrs)
{
var name = attr["name"]?.ToString() ?? "";
var displayKey = attr["displayKey"]?.ToString() ?? "";
var key = attr["displayKey"]?.ToString() ?? "";
var val = attr["value"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(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;
}
attrs[key] = val;
}
}
}
// Primaer nach name-Feld mappen, Fallback auf displayKey
string Get(string name, string displayKey = null)
{
if (byName.TryGetValue(name, out var v)) return v;
if (displayKey != null && byDisplayKey.TryGetValue(displayKey, out v)) return v;
return "";
}
contact.FirstName = Get("firstname", "NAME");
contact.LastName = Get("familyname", "SURNAME");
contact.Company = Get("company", "COMPANY");
contact.JobTitle = Get("jobtitle", "JOB_TITLE");
contact.Email = Get("e-mail", "EMAIL");
contact.PhoneWork = Get("phone", "PHONE_NUMBER");
contact.PhoneMobile = Get("mobile", "MOBILE_PHONE_NUMBER");
contact.PhoneHome = Get("homephone", "PRIVATE_PHONE_NUMBER");
contact.Fax = Get("fax", "FAX_NUMBER");
contact.Street = Get("street", "STREET");
contact.City = Get("city", "CITY");
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");
contact.FirstName = attrs.GetValueOrDefault("NAME", "");
contact.LastName = attrs.GetValueOrDefault("SURNAME", "");
contact.Company = attrs.GetValueOrDefault("COMPANY", "");
contact.JobTitle = attrs.GetValueOrDefault("JOB_TITLE", "");
contact.Email = attrs.GetValueOrDefault("EMAIL", "");
contact.PhoneWork = attrs.GetValueOrDefault("OFFICE_PHONE_NUMBER", "");
contact.PhoneMobile = attrs.GetValueOrDefault("MOBILE_PHONE_NUMBER", "");
contact.PhoneHome = attrs.GetValueOrDefault("PRIVATE_PHONE_NUMBER", "");
contact.Fax = attrs.GetValueOrDefault("FAX_NUMBER", "");
contact.Street = attrs.GetValueOrDefault("STREET", "");
contact.City = attrs.GetValueOrDefault("CITY", "");
contact.PostalCode = attrs.GetValueOrDefault("POSTAL_CODE", "");
contact.State = attrs.GetValueOrDefault("STATE", "");
contact.Country = attrs.GetValueOrDefault("COUNTRY", "");
contact.Website = attrs.GetValueOrDefault("URL", "");
contact.Notes = attrs.GetValueOrDefault("NOTE", "");
contact.Salutation = attrs.GetValueOrDefault("SALUTATION", "");
contact.Title = attrs.GetValueOrDefault("TITLE", "");
contact.Birthday = attrs.GetValueOrDefault("BIRTHDAY", "");
return 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();
foreach (var (dk, n, v) in fields)
{
if (!string.IsNullOrEmpty(v))
arr.Add(new JObject { ["displayKey"] = dk, ["name"] = n, ["value"] = v });
}
return arr;
if (!string.IsNullOrEmpty(value))
attrs.Add(new JObject { ["displayKey"] = displayKey, ["name"] = name, ["value"] = value });
}
// Block-Struktur wie von der Starface erwartet
var contactBlock = MakeAttrs(
("NAME", "firstname", contact.FirstName),
("SURNAME", "familyname", contact.LastName),
("COMPANY", "company", contact.Company)
);
var addressBlock = MakeAttrs(
("USER_DEFINED", "street", contact.Street),
("POSTAL_CODE", "postcode", contact.PostalCode),
("USER_DEFINED", "city", contact.City),
("USER_DEFINED", "state", contact.State),
("USER_DEFINED", "country", contact.Country)
);
var phoneBlock = MakeAttrs(
("PHONE_NUMBER", "phone", contact.PhoneWork),
("PRIVATE_PHONE_NUMBER", "homephone", contact.PhoneHome),
("MOBILE_PHONE_NUMBER", "mobile", contact.PhoneMobile),
("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
});
AddAttr("NAME", "firstName", contact.FirstName);
AddAttr("SURNAME", "lastName", contact.LastName);
AddAttr("COMPANY", "company", contact.Company);
AddAttr("JOB_TITLE", "jobTitle", contact.JobTitle);
AddAttr("EMAIL", "email", contact.Email);
AddAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.PhoneWork);
AddAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.PhoneMobile);
AddAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.PhoneHome);
AddAttr("FAX_NUMBER", "fax", contact.Fax);
AddAttr("STREET", "street", contact.Street);
AddAttr("CITY", "city", contact.City);
AddAttr("POSTAL_CODE", "postalCode", contact.PostalCode);
AddAttr("STATE", "state", contact.State);
AddAttr("COUNTRY", "country", contact.Country);
AddAttr("URL", "website", contact.Website);
AddAttr("NOTE", "notes", contact.Notes);
AddAttr("SALUTATION", "salutation", contact.Salutation);
AddAttr("TITLE", "title", contact.Title);
AddAttr("BIRTHDAY", "birthday", contact.Birthday);
return new JObject
{
["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);
/// <summary>
/// Findet einen passenden Kontakt in der Kandidatenliste.
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
/// auf der anderen auch gefuellt (und gleich) sein.
/// Ein leeres Feld auf einer Seite und ein gefuelltes auf der anderen
/// bedeutet: verschiedene Kontakte.
/// </summary>
private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
{
if (candidates == null || candidates.Count == 0) return null;
foreach (var c in candidates)
// Erst E-Mail-Match
if (!string.IsNullOrEmpty(contact.Email))
{
if (IsMatch(contact, c))
return c;
var byEmail = candidates.FirstOrDefault(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;
}
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)
{
var result = new SyncResult
@ -122,10 +49,10 @@ namespace StarfaceOutlookSync.Services
try
{
// Starface verbinden
Log("Verbinde mit Starface...");
using (var starface = new StarfaceApiClient(profile.StarfaceConnection))
{
starface.OnDebug += (msg) => Log(msg);
var loginOk = await starface.LoginAsync();
if (!loginOk)
{
@ -134,6 +61,10 @@ namespace StarfaceOutlookSync.Services
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
Log("Lade Outlook-Kontakte...");
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
@ -143,290 +74,144 @@ namespace StarfaceOutlookSync.Services
var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook);
Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
// Bestehende Mappings laden
var mappings = _profileManager.GetMappings(profile.Id);
// Sets fuer schnellen Lookup
var mappingByOutlook = new Dictionary<string, SyncMapping>();
var mappingByStarface = new Dictionary<string, SyncMapping>();
foreach (var m in mappings)
// Outlook -> Starface
if (profile.SyncDirection == SyncDirection.Both ||
profile.SyncDirection == SyncDirection.OutlookToStarface)
{
if (!string.IsNullOrEmpty(m.OutlookEntryId))
mappingByOutlook[m.OutlookEntryId] = m;
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)
Log("Synchronisiere Outlook -> Starface...");
foreach (var oc in outlookContacts)
{
try
{
// Duplikat-Check: existiert der Kontakt schon in der Starface?
var match = FindMatch(oc, unmappedStarface);
if (match != null)
SyncMapping existing = null;
if (!string.IsNullOrEmpty(oc.OutlookEntryId))
mappingByOutlook.TryGetValue(oc.OutlookEntryId, out existing);
if (existing != null)
{
// Existiert schon -> verknuepfen und updaten
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook))
var hash = oc.GetHash();
if (hash != existing.LastSyncHash)
{
newMappings.Add(new SyncMapping
if (await starface.UpdateContactAsync(existing.StarfaceId, oc, profile.StarfaceAddressBook))
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = match.StarfaceId,
LastSyncHash = oc.GetHash()
});
processedStarfaceIds.Add(match.StarfaceId);
unmappedStarface.Remove(match);
result.Updated++;
Log($" Verknuepft (OL->SF): {oc.DisplayName}");
existing.LastSyncHash = hash;
result.Updated++;
}
}
}
else
{
// Neu -> in Starface erstellen
Log($" Erstelle in Starface: {oc.DisplayName}");
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
var match = FindMatch(oc, starfaceContacts);
if (match != null && !string.IsNullOrEmpty(match.StarfaceId))
{
newMappings.Add(new SyncMapping
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook))
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId,
LastSyncHash = oc.GetHash()
});
result.Created++;
Log($" Erstellt (OL->SF): {oc.DisplayName}");
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = match.StarfaceId,
LastSyncHash = oc.GetHash()
});
result.Updated++;
}
}
else
{
Log($" FEHLER: Kontakt konnte nicht erstellt werden: {oc.DisplayName}");
result.Errors++;
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
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)
{
result.Errors++;
result.ErrorMessages.Add($"OL->SF {oc.DisplayName}: {ex.Message}");
result.ErrorMessages.Add($"{oc.DisplayName}: {ex.Message}");
}
}
}
// ============================================
// Phase 3: Neue Starface-Kontakte (ohne Mapping)
// ============================================
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)
// Starface -> Outlook
if (profile.SyncDirection == SyncDirection.Both ||
profile.SyncDirection == SyncDirection.StarfaceToOutlook)
{
var unmappedStarface = starfaceContacts
.Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId))
.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)
Log("Synchronisiere Starface -> Outlook...");
foreach (var sc in starfaceContacts)
{
try
{
// Duplikat-Check: existiert der Kontakt schon in Outlook?
var match = FindMatch(sc, unmappedOutlook);
if (match != null)
SyncMapping existing = null;
if (!string.IsNullOrEmpty(sc.StarfaceId))
mappingByStarface.TryGetValue(sc.StarfaceId, out existing);
if (existing != null)
{
// Existiert schon -> verknuepfen und updaten
if (_outlookService.UpdateContact(match.OutlookEntryId, sc))
var hash = sc.GetHash();
if (hash != existing.LastSyncHash)
{
newMappings.Add(new SyncMapping
if (_outlookService.UpdateContact(existing.OutlookEntryId, sc))
{
ProfileId = profile.Id,
OutlookEntryId = match.OutlookEntryId,
StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
processedOutlookIds.Add(match.OutlookEntryId);
unmappedOutlook.Remove(match);
result.Updated++;
Log($" Verknuepft (SF->OL): {sc.DisplayName}");
existing.LastSyncHash = hash;
result.Updated++;
}
}
}
else
{
// Neu -> in Outlook erstellen
var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath);
if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId))
var match = FindMatch(sc, outlookContacts);
if (match != null && !string.IsNullOrEmpty(match.OutlookEntryId))
{
newMappings.Add(new SyncMapping
if (_outlookService.UpdateContact(match.OutlookEntryId, sc))
{
ProfileId = profile.Id,
OutlookEntryId = created.OutlookEntryId,
StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
result.Created++;
Log($" Erstellt (SF->OL): {sc.DisplayName}");
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = match.OutlookEntryId,
StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
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)
{
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.SaveMappings(profile.Id, mappings);
await starface.LogoutAsync();
Log($"Fertig: {result.Created} erstellt, {result.Updated} aktualisiert, {result.Errors} Fehler");
Log("Synchronisation abgeschlossen!");
}
}
catch (Exception ex)

View File

@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.0.23</Version>
<AssemblyVersion>0.0.0.23</AssemblyVersion>
<FileVersion>0.0.0.23</FileVersion>
<Version>0.0.0.2</Version>
<AssemblyVersion>0.0.0.2</AssemblyVersion>
<FileVersion>0.0.0.2</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
@ -19,6 +19,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Office.Interop.Outlook" Version="15.0.4797.1004" />
</ItemGroup>
</Project>

View File

@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label
{
Text = "Version 0.0.0.23",
Text = "Version 0.0.0.2",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
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 ContextMenuStrip _trayMenu;
private ListView _profileList;
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnInfo;
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnInfo;
private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer;
private volatile bool _syncRunning = false;
public MainForm()
{
@ -30,57 +29,15 @@ namespace StarfaceOutlookSync.UI
SetupTrayIcon();
SetupAutoSync();
RefreshProfileList();
// Einstellungen laden und anwenden
var settings = UserSettings.Load();
settings.ApplyOutlookSecuritySetting();
if (settings.StartMinimized)
{
WindowState = FormWindowState.Minimized;
ShowInTaskbar = 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)
{
// Beim ersten Anzeigen pruefen ob minimiert gestartet werden soll
if (!IsHandleCreated)
{
var settings = UserSettings.Load();
if (settings.StartMinimized)
{
CreateHandle();
value = false;
}
}
base.SetVisibleCore(value);
}
private void InitializeComponent()
{
Text = "Starface Kontakt-Sync";
Size = new Size(830, 450);
MinimumSize = new Size(830, 350);
Size = new Size(620, 450);
MinimumSize = new Size(500, 350);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9);
Icon = AppIcon.GetIcon();
// Profil-Liste
_profileList = new ListView
@ -116,19 +73,13 @@ namespace StarfaceOutlookSync.UI
_btnDelete = new Button { Text = "Loeschen", Width = 80, Height = 30 };
_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();
_btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
_btnReset.Click += (s, e) => ResetSync();
_btnSettings = new Button { Text = "Einstellungen", Width = 95, Height = 30 };
_btnSettings.Click += (s, e) => ShowSettings();
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
_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, _btnInfo });
// Statusbar
_statusBar = new StatusStrip();
@ -143,26 +94,10 @@ namespace StarfaceOutlookSync.UI
private void SetupTrayIcon()
{
_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("-");
// Schnell-Sync fuer jedes Profil
var profiles = _profileManager.GetProfiles();
foreach (var p in profiles.Where(p => p.Enabled))
{
@ -176,8 +111,17 @@ namespace StarfaceOutlookSync.UI
if (profiles.Any(p => p.Enabled))
_trayMenu.Items.Add("-");
_trayMenu.Items.Add("Ueber", null, (s, e) => ShowAbout());
_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()
@ -245,7 +189,7 @@ namespace StarfaceOutlookSync.UI
}
// Tray-Menu aktualisieren
UpdateTrayMenu();
SetupTrayIcon();
}
private void NewProfile()
@ -284,36 +228,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()
{
if (_profileList.SelectedItems.Count == 0)
@ -337,13 +251,6 @@ namespace StarfaceOutlookSync.UI
private async Task RunSync(SyncProfile profile)
{
if (_syncRunning)
{
SetStatus("Sync laeuft bereits, bitte warten...");
return;
}
_syncRunning = true;
try
{
SetStatus($"Synchronisiere '{profile.Name}'...");
@ -366,10 +273,6 @@ namespace StarfaceOutlookSync.UI
ex.Message, ToolTipIcon.Error);
SetStatus($"Fehler: {ex.Message}");
}
finally
{
_syncRunning = false;
}
}
private void SetStatus(string text)
@ -380,14 +283,6 @@ namespace StarfaceOutlookSync.UI
_statusLabel.Text = text;
}
private void ShowSettings()
{
using (var settings = new SettingsForm())
{
settings.ShowDialog(this);
}
}
private void ShowAbout()
{
using (var about = new AboutForm())

View File

@ -1,125 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;
namespace StarfaceOutlookSync.UI
{
public class OutlookFolderBrowserForm : Form
{
private TreeView _tree;
private Button _btnOk, _btnCancel;
public string SelectedFolderPath { get; private set; } = "";
public string SelectedFolderName { get; private set; } = "";
public OutlookFolderBrowserForm(List<string> folderPaths, string currentSelection)
{
InitializeComponent();
BuildTree(folderPaths, currentSelection);
}
private void InitializeComponent()
{
Text = "Outlook-Ordner waehlen";
Size = new Size(400, 450);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9);
var lblInfo = new Label
{
Text = "Bitte waehlen Sie den Outlook-Kontaktordner fuer die Synchronisation:",
Left = 12, Top = 12, Width = 360, Height = 36
};
_tree = new TreeView
{
Left = 12, Top = 52, Width = 360, Height = 310,
HideSelection = false,
ShowLines = true,
ShowPlusMinus = true,
ShowRootLines = true
};
_tree.DoubleClick += (s, e) => { if (_tree.SelectedNode?.Tag != null) SelectAndClose(); };
_btnOk = new Button
{
Text = "OK", Left = 210, Top = 372, Width = 75, Height = 28,
DialogResult = DialogResult.None
};
_btnOk.Click += (s, e) => SelectAndClose();
_btnCancel = new Button
{
Text = "Abbrechen", Left = 292, Top = 372, Width = 80, Height = 28,
DialogResult = DialogResult.Cancel
};
Controls.AddRange(new Control[] { lblInfo, _tree, _btnOk, _btnCancel });
AcceptButton = _btnOk;
CancelButton = _btnCancel;
}
private void BuildTree(List<string> folderPaths, string currentSelection)
{
_tree.Nodes.Clear();
var nodeMap = new Dictionary<string, TreeNode>();
foreach (var path in folderPaths)
{
// Pfad: \\Store\Kontakte\Unterordner
var parts = path.TrimStart('\\').Split('\\');
string runningPath = "";
TreeNode parent = null;
for (int i = 0; i < parts.Length; i++)
{
runningPath += "\\" + parts[i];
if (!nodeMap.ContainsKey(runningPath))
{
var node = new TreeNode(parts[i]);
// Nur Blatt-Knoten oder bekannte Pfade sind waehlbar
if (i == parts.Length - 1)
node.Tag = path; // Vollstaendiger Pfad
if (parent == null)
_tree.Nodes.Add(node);
else
parent.Nodes.Add(node);
nodeMap[runningPath] = node;
// Aktuellen Ordner vorauswaehlen
if (path == currentSelection)
{
_tree.SelectedNode = node;
node.EnsureVisible();
}
}
parent = nodeMap[runningPath];
}
}
_tree.ExpandAll();
}
private void SelectAndClose()
{
if (_tree.SelectedNode?.Tag == null)
{
MessageBox.Show("Bitte einen Kontaktordner waehlen.", "Hinweis",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SelectedFolderPath = _tree.SelectedNode.Tag.ToString();
SelectedFolderName = _tree.SelectedNode.Text;
DialogResult = DialogResult.OK;
Close();
}
}
}

View File

@ -15,18 +15,14 @@ namespace StarfaceOutlookSync.UI
private readonly bool _isNew;
// Controls
private TextBox _txtName, _txtHost, _txtPort, _txtLoginId, _txtPassword, _txtOutlookFolder;
private TextBox _txtName, _txtHost, _txtPort, _txtLoginId, _txtPassword;
private CheckBox _chkSsl, _chkEnabled;
private ComboBox _cmbAddressBook, _cmbDirection;
private Button _btnBrowseFolder;
private ComboBox _cmbAddressBook, _cmbOutlookFolder, _cmbDirection;
private NumericUpDown _numAutoSync;
private Button _btnTest, _btnLoadBooks, _btnSave, _btnCancel;
private Label _lblTestResult;
private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>();
private List<string> _outlookFolderPaths = new List<string>();
private string _selectedOutlookPath = "";
private string _selectedOutlookName = "";
public ProfileEditorForm(SyncProfile profile)
{
@ -91,11 +87,8 @@ namespace StarfaceOutlookSync.UI
panel.Controls.Add(MakeSectionLabel("Outlook-Einstellungen", 12, y)); y += 26;
panel.Controls.Add(MakeLabel("Kontakte-Ordner:", 12, y)); y += 22;
_txtOutlookFolder = new TextBox { Left = 12, Top = y, Width = 330, ReadOnly = true, BackColor = SystemColors.Window };
_btnBrowseFolder = new Button { Text = "Durchsuchen...", Left = 348, Top = y - 1, Width = 84, Height = 24 };
_btnBrowseFolder.Click += (s, e) => BrowseOutlookFolder();
panel.Controls.Add(_txtOutlookFolder);
panel.Controls.Add(_btnBrowseFolder); y += 32;
_cmbOutlookFolder = new ComboBox { Left = 12, Top = y, Width = 420, DropDownStyle = ComboBoxStyle.DropDownList };
panel.Controls.Add(_cmbOutlookFolder); y += 32;
panel.Controls.Add(MakeLabel("Sync-Richtung:", 12, y)); y += 22;
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
@ -139,15 +132,18 @@ namespace StarfaceOutlookSync.UI
{
using (var outlook = new OutlookContactsService())
{
_outlookFolderPaths = outlook.GetContactFolderPaths();
var folders = outlook.GetContactFolderPaths();
_cmbOutlookFolder.Items.Clear();
foreach (var f in folders)
_cmbOutlookFolder.Items.Add(f);
if (folders.Count > 0)
_cmbOutlookFolder.SelectedIndex = 0;
}
}
catch (Exception ex)
catch
{
MessageBox.Show(
$"Outlook-Kontaktordner konnten nicht geladen werden:\n{ex.Message}\n\nIst Outlook gestartet?",
"Outlook-Verbindung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_outlookFolderPaths = new List<string>();
_cmbOutlookFolder.Items.Add("\\\\Kontakte");
_cmbOutlookFolder.SelectedIndex = 0;
}
// Bestehende Werte laden
@ -164,10 +160,15 @@ namespace StarfaceOutlookSync.UI
_cmbDirection.SelectedIndex = (int)_existingProfile.SyncDirection;
// Outlook-Ordner
_selectedOutlookPath = _existingProfile.OutlookFolderPath;
_selectedOutlookName = _existingProfile.OutlookFolderName;
_txtOutlookFolder.Text = _selectedOutlookPath;
// Outlook-Ordner auswaehlen
for (int i = 0; i < _cmbOutlookFolder.Items.Count; i++)
{
if (_cmbOutlookFolder.Items[i].ToString() == _existingProfile.OutlookFolderPath)
{
_cmbOutlookFolder.SelectedIndex = i;
break;
}
}
// Adressbuch
if (_existingProfile.StarfaceAddressBook != null)
@ -177,26 +178,6 @@ namespace StarfaceOutlookSync.UI
_cmbAddressBook.SelectedIndex = 0;
}
}
else if (_outlookFolderPaths.Count > 0)
{
// Standard-Ordner vorauswaehlen
_selectedOutlookPath = _outlookFolderPaths[0];
_selectedOutlookName = _selectedOutlookPath.Substring(_selectedOutlookPath.LastIndexOf('\\') + 1);
_txtOutlookFolder.Text = _selectedOutlookPath;
}
}
private void BrowseOutlookFolder()
{
using (var browser = new OutlookFolderBrowserForm(_outlookFolderPaths, _selectedOutlookPath))
{
if (browser.ShowDialog(this) == DialogResult.OK)
{
_selectedOutlookPath = browser.SelectedFolderPath;
_selectedOutlookName = browser.SelectedFolderName;
_txtOutlookFolder.Text = _selectedOutlookPath;
}
}
}
private StarfaceConnection GetConnection()
@ -311,12 +292,10 @@ namespace StarfaceOutlookSync.UI
return;
}
if (string.IsNullOrEmpty(_selectedOutlookPath))
{
MessageBox.Show("Bitte einen Outlook-Kontaktordner waehlen.",
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var selectedFolder = _cmbOutlookFolder.SelectedItem?.ToString() ?? "";
var folderName = selectedFolder;
if (folderName.Contains("\\"))
folderName = folderName.Substring(folderName.LastIndexOf('\\') + 1);
var profile = new SyncProfile
{
@ -324,8 +303,8 @@ namespace StarfaceOutlookSync.UI
Name = _txtName.Text.Trim(),
StarfaceConnection = GetConnection(),
StarfaceAddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
OutlookFolderPath = _selectedOutlookPath,
OutlookFolderName = _selectedOutlookName,
OutlookFolderPath = selectedFolder,
OutlookFolderName = folderName,
SyncDirection = (SyncDirection)_cmbDirection.SelectedIndex,
Enabled = _chkEnabled.Checked,
AutoSyncIntervalMinutes = (int)_numAutoSync.Value,
@ -333,23 +312,9 @@ namespace StarfaceOutlookSync.UI
};
if (_isNew)
{
_pm.AddProfile(profile);
}
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);
}
DialogResult = DialogResult.OK;
Close();

View File

@ -1,91 +0,0 @@
using System.Drawing;
using System.Windows.Forms;
using StarfaceOutlookSync.Models;
namespace StarfaceOutlookSync.UI
{
public class SettingsForm : Form
{
private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook;
private Button _btnSave, _btnCancel;
private readonly UserSettings _settings;
public SettingsForm()
{
_settings = UserSettings.Load();
InitializeComponent();
}
private void InitializeComponent()
{
Text = "Einstellungen";
Size = new Size(380, 250);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9);
_chkStartMinimized = new CheckBox
{
Text = "Minimiert starten (nur im System Tray)",
Left = 20, Top = 24, AutoSize = true,
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
{
Text = "Speichern", Left = 95, Top = 170, Width = 85, Height = 28,
DialogResult = DialogResult.None
};
_btnSave.Click += (s, e) => Save();
_btnCancel = new Button
{
Text = "Abbrechen", Left = 189, Top = 170, Width = 85, Height = 28,
DialogResult = DialogResult.Cancel
};
Size = new Size(380, 260);
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint, _btnSave, _btnCancel });
AcceptButton = _btnSave;
CancelButton = _btnCancel;
}
private void Save()
{
_settings.StartMinimized = _chkStartMinimized.Checked;
_settings.SyncOnStart = _chkSyncOnStart.Checked;
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
_settings.Save();
DialogResult = DialogResult.OK;
Close();
}
}
}

View File

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