From 59d4c094a994346fac16ee49e80f07c88cd62bf9 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 3 Apr 2026 12:01:49 +0200 Subject: [PATCH] Replace Outlook Interop NuGet with dynamic COM binding The NuGet package Microsoft.Office.Interop.Outlook v15 tried to load Office v15 PIA which fails on Office 2016/2019/2024 (v16). Switch to dynamic COM (late binding) which works with ANY Outlook Classic version without needing a specific Interop DLL. All Outlook properties and methods are accessed via dynamic dispatch. Also use 1-based COM collection indexing instead of foreach to avoid COM enumerator issues. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Services/OutlookContactsService.cs | 203 +++++++++++------- .../StarfaceOutlookSync.csproj | 1 - 2 files changed, 127 insertions(+), 77 deletions(-) diff --git a/src/StarfaceOutlookSync/Services/OutlookContactsService.cs b/src/StarfaceOutlookSync/Services/OutlookContactsService.cs index db34ecc9..da7d78eb 100644 --- a/src/StarfaceOutlookSync/Services/OutlookContactsService.cs +++ b/src/StarfaceOutlookSync/Services/OutlookContactsService.cs @@ -2,46 +2,62 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using StarfaceOutlookSync.Models; -using Outlook = Microsoft.Office.Interop.Outlook; namespace StarfaceOutlookSync.Services { + /// + /// Zugriff auf Outlook-Kontakte per dynamic COM. + /// Funktioniert mit jeder Outlook Classic Version (2013-2024) + /// ohne Abhaengigkeit von einer bestimmten Interop-DLL. + /// public class OutlookContactsService : IDisposable { - private Outlook.Application _outlookApp; + private dynamic _outlookApp; private bool _weStartedOutlook; - // Marshal.GetActiveObject existiert nicht in .NET 8, daher P/Invoke + // OlDefaultFolders.olFolderContacts = 10 + private const int OlFolderContacts = 10; + // OlItemType.olContactItem = 2 + private const int OlContactItem = 2; + [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 clsid = Type.GetTypeFromProgID(progId, true).GUID; - GetActiveObject(clsid, IntPtr.Zero, out var obj); + var type = Type.GetTypeFromProgID(progId, false); + if (type == null) return null; + GetActiveObject(type.GUID, IntPtr.Zero, out var obj); return obj; } - private Outlook.Application GetOutlookApp() + private dynamic GetOutlookApp() { if (_outlookApp != null) return _outlookApp; // Versuch 1: Laufende Outlook-Instanz finden try { - _outlookApp = (Outlook.Application)GetActiveComObject("Outlook.Application"); - _weStartedOutlook = false; - return _outlookApp; + var obj = GetActiveComObject("Outlook.Application"); + if (obj != null) + { + _outlookApp = obj; + _weStartedOutlook = false; + return _outlookApp; + } } catch { } // Versuch 2: Outlook per COM starten try { - var outlookType = Type.GetTypeFromProgID("Outlook.Application"); + var outlookType = Type.GetTypeFromProgID("Outlook.Application", false); if (outlookType != null) { - _outlookApp = (Outlook.Application)Activator.CreateInstance(outlookType); + _outlookApp = Activator.CreateInstance(outlookType); _weStartedOutlook = true; return _outlookApp; } @@ -65,18 +81,21 @@ namespace StarfaceOutlookSync.Services var app = GetOutlookApp(); var ns = app.GetNamespace("MAPI"); - // Alle Stores durchgehen (jedes Konto, jede PST-Datei etc.) - foreach (Outlook.Store store in ns.Stores) + // 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 '{store.DisplayName}': {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error scanning store {i}: {ex.Message}"); } } @@ -85,121 +104,141 @@ namespace StarfaceOutlookSync.Services { try { - var defaultFolder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts); - folders.Add(defaultFolder.FolderPath); + var defaultFolder = ns.GetDefaultFolder(OlFolderContacts); + folders.Add((string)defaultFolder.FolderPath); Marshal.ReleaseComObject(defaultFolder); } 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(Outlook.MAPIFolder folder, List paths) + private void FindContactFoldersRecursive(dynamic folder, List paths) { try { - // Kontaktordner erkennen: DefaultItemType ODER Ordnername enthaelt "Kontakt"/"Contact" - if (folder.DefaultItemType == Outlook.OlItemType.olContactItem) + if ((int)folder.DefaultItemType == OlContactItem) { - if (!paths.Contains(folder.FolderPath)) - paths.Add(folder.FolderPath); + string path = folder.FolderPath; + if (!paths.Contains(path)) + paths.Add(path); } - // Alle Unterordner durchsuchen - foreach (Outlook.MAPIFolder sub in folder.Folders) + var subFolders = folder.Folders; + for (int i = 1; i <= (int)subFolders.Count; i++) { try { + var sub = subFolders[i]; FindContactFoldersRecursive(sub, paths); - } - catch { } - finally - { Marshal.ReleaseComObject(sub); } + catch { } } + Marshal.ReleaseComObject(subFolders); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Error scanning folder '{folder.Name}': {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error scanning folder: {ex.Message}"); } } - private Outlook.MAPIFolder GetFolderByPath(string folderPath) + private dynamic GetFolderByPath(string folderPath) { var app = GetOutlookApp(); var ns = app.GetNamespace("MAPI"); - // Standard-Kontaktordner als Fallback if (string.IsNullOrEmpty(folderPath)) - return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts); + return ns.GetDefaultFolder(OlFolderContacts); try { - // Pfad durchlaufen var parts = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); - Outlook.MAPIFolder current = null; + dynamic current = null; - foreach (Outlook.Store store in ns.Stores) + var stores = ns.Stores; + for (int i = 1; i <= (int)stores.Count; i++) { - if (store.GetRootFolder().Name == parts[0] || - store.GetRootFolder().FolderPath.TrimStart('\\') == parts[0]) + var store = stores[i]; + var root = store.GetRootFolder(); + string rootName = root.Name; + + if (rootName == parts[0]) { - current = store.GetRootFolder(); + current = root; break; } + Marshal.ReleaseComObject(root); + Marshal.ReleaseComObject(store); } + Marshal.ReleaseComObject(stores); if (current == null) - return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts); + return ns.GetDefaultFolder(OlFolderContacts); for (int i = 1; i < parts.Length; i++) { bool found = false; - foreach (Outlook.MAPIFolder sub in current.Folders) + var subFolders = current.Folders; + for (int j = 1; j <= (int)subFolders.Count; j++) { - if (sub.Name == parts[i]) + var sub = subFolders[j]; + if ((string)sub.Name == parts[i]) { current = sub; found = true; break; } + Marshal.ReleaseComObject(sub); } + Marshal.ReleaseComObject(subFolders); + if (!found) - return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts); + return ns.GetDefaultFolder(OlFolderContacts); } return current; } catch { - return ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts); + return ns.GetDefaultFolder(OlFolderContacts); } } public List GetContacts(string folderPath) { var contacts = new List(); - try { var folder = GetFolderByPath(folderPath); var items = folder.Items; - foreach (var item in items) + for (int i = 1; i <= (int)items.Count; i++) { - if (item is Outlook.ContactItem ci) + dynamic item = null; + try { - contacts.Add(MapFromOutlook(ci)); - Marshal.ReleaseComObject(ci); + item = items[i]; + // Nur ContactItems verarbeiten (Class = 40 = olContact) + if ((int)item.Class == 40) + { + contacts.Add(MapFromOutlook(item)); + } + } + catch { } + finally + { + if (item != null) Marshal.ReleaseComObject(item); } } @@ -219,7 +258,8 @@ namespace StarfaceOutlookSync.Services try { var folder = GetFolderByPath(folderPath); - var ci = (Outlook.ContactItem)folder.Items.Add(Outlook.OlItemType.olContactItem); + var items = folder.Items; + dynamic ci = items.Add(OlContactItem); MapToOutlook(contact, ci); ci.Save(); @@ -227,6 +267,7 @@ namespace StarfaceOutlookSync.Services contact.OutlookEntryId = ci.EntryID; Marshal.ReleaseComObject(ci); + Marshal.ReleaseComObject(items); Marshal.ReleaseComObject(folder); return contact; @@ -244,7 +285,7 @@ namespace StarfaceOutlookSync.Services { var app = GetOutlookApp(); var ns = app.GetNamespace("MAPI"); - var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId); + dynamic ci = ns.GetItemFromID(entryId); MapToOutlook(contact, ci); ci.Save(); @@ -267,7 +308,7 @@ namespace StarfaceOutlookSync.Services { var app = GetOutlookApp(); var ns = app.GetNamespace("MAPI"); - var ci = (Outlook.ContactItem)ns.GetItemFromID(entryId); + dynamic ci = ns.GetItemFromID(entryId); ci.Delete(); Marshal.ReleaseComObject(ci); @@ -282,36 +323,46 @@ namespace StarfaceOutlookSync.Services } } - private UnifiedContact MapFromOutlook(Outlook.ContactItem ci) + private UnifiedContact MapFromOutlook(dynamic ci) { return new UnifiedContact { - 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") : "" + OutlookEntryId = (string)(ci.EntryID ?? ""), + FirstName = (string)(ci.FirstName ?? ""), + LastName = (string)(ci.LastName ?? ""), + Company = (string)(ci.CompanyName ?? ""), + JobTitle = (string)(ci.JobTitle ?? ""), + Email = (string)(ci.Email1Address ?? ""), + EmailSecondary = (string)(ci.Email2Address ?? ""), + PhoneWork = (string)(ci.BusinessTelephoneNumber ?? ""), + PhoneMobile = (string)(ci.MobileTelephoneNumber ?? ""), + PhoneHome = (string)(ci.HomeTelephoneNumber ?? ""), + Fax = (string)(ci.BusinessFaxNumber ?? ""), + Street = (string)(ci.BusinessAddressStreet ?? ""), + City = (string)(ci.BusinessAddressCity ?? ""), + PostalCode = (string)(ci.BusinessAddressPostalCode ?? ""), + State = (string)(ci.BusinessAddressState ?? ""), + Country = (string)(ci.BusinessAddressCountry ?? ""), + Website = (string)(ci.WebPage ?? ""), + Notes = (string)(ci.Body ?? ""), + Salutation = (string)(ci.Title ?? ""), + Birthday = GetBirthdayString(ci) }; } - private void MapToOutlook(UnifiedContact contact, Outlook.ContactItem ci) + private string GetBirthdayString(dynamic ci) + { + try + { + DateTime bday = ci.Birthday; + if (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.LastName = contact.LastName; @@ -348,7 +399,7 @@ namespace StarfaceOutlookSync.Services { try { _outlookApp.Quit(); } catch { } } - Marshal.ReleaseComObject(_outlookApp); + try { Marshal.ReleaseComObject(_outlookApp); } catch { } _outlookApp = null; } } diff --git a/src/StarfaceOutlookSync/StarfaceOutlookSync.csproj b/src/StarfaceOutlookSync/StarfaceOutlookSync.csproj index acce1f2d..7cf75307 100644 --- a/src/StarfaceOutlookSync/StarfaceOutlookSync.csproj +++ b/src/StarfaceOutlookSync/StarfaceOutlookSync.csproj @@ -19,7 +19,6 @@ -