#if WINDOWS using System.Collections; using System.Diagnostics; using System.Runtime.InteropServices; namespace MeetingAssistant.MeetingNotes; internal static class OutlookClassicCom { private const int OutlookCalendarFolder = 9; public static OutlookCalendarSession? OpenDefaultCalendar( ILogger logger, string logContext) { EnsureOutlookRunning(logger, logContext); object? application = null; object? session = null; object? calendar = null; object? items = null; try { var applicationType = Type.GetTypeFromProgID("Outlook.Application"); if (applicationType is null) { return null; } application = Activator.CreateInstance(applicationType); if (application is null) { return null; } session = GetProperty(application, "Session"); calendar = Invoke(session!, "GetDefaultFolder", OutlookCalendarFolder); items = GetProperty(calendar!, "Items"); Invoke(items!, "Sort", "[Start]"); SetProperty(items!, "IncludeRecurrences", true); return new OutlookCalendarSession(application, session, calendar, items!); } catch { ReleaseComObject(items); ReleaseComObject(calendar); ReleaseComObject(session); ReleaseComObject(application); throw; } } public static Task RunStaAsync( Func action, CancellationToken cancellationToken, string threadName) { var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var thread = new Thread(() => { try { completion.TrySetResult(action()); } catch (Exception exception) { completion.TrySetException(exception); } }) { IsBackground = true, Name = threadName }; thread.SetApartmentState(ApartmentState.STA); var registration = cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken)); _ = completion.Task.ContinueWith( _ => registration.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); thread.Start(); return completion.Task; } public static IEnumerable EnumerateItems(object items) { if (items is IEnumerable enumerable) { foreach (var item in enumerable) { yield return item; } } } public static bool IsAppointment(object item) { try { return Convert.ToInt32(GetProperty(item, "Class")) == 26; } catch { return false; } } public static bool IsCanceledAppointment(object appointment) { var meetingStatus = TryGetProperty(appointment, "MeetingStatus"); if (meetingStatus is not null) { try { var status = Convert.ToInt32(meetingStatus); if (status is 5 or 7) { return true; } } catch (Exception exception) when (exception is FormatException or InvalidCastException or OverflowException) { // Fall back to subject prefixes when Outlook exposes a non-numeric status value. } } var subject = Convert.ToString(TryGetProperty(appointment, "Subject")) ?? ""; return SubjectIndicatesCancellation(subject); } internal static bool SubjectIndicatesCancellation(string subject) { var normalized = subject.TrimStart(); return normalized.StartsWith("Canceled:", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("Cancelled:", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("Abgesagt:", StringComparison.OrdinalIgnoreCase); } public static DateTimeOffset ToLocalOffset(DateTime value) { return new DateTimeOffset(DateTime.SpecifyKind(value, DateTimeKind.Local)); } public static object? TryGetProperty(object target, string name) { try { return GetProperty(target, name); } catch { return null; } } public static object? GetProperty(object target, string name) { return target.GetType().InvokeMember( name, System.Reflection.BindingFlags.GetProperty, null, target, null); } public static void SetProperty(object target, string name, object value) { target.GetType().InvokeMember( name, System.Reflection.BindingFlags.SetProperty, null, target, [value]); } public static object? Invoke(object target, string name, params object[] arguments) { return target.GetType().InvokeMember( name, System.Reflection.BindingFlags.InvokeMethod, null, target, arguments); } public static void ReleaseComObject(object? value) { if (value is not null && Marshal.IsComObject(value)) { Marshal.FinalReleaseComObject(value); } } private static void EnsureOutlookRunning(ILogger logger, string logContext) { if (Process.GetProcessesByName("OUTLOOK").Length > 0) { return; } try { Process.Start(new ProcessStartInfo { FileName = "outlook.exe", UseShellExecute = true }); Thread.Sleep(TimeSpan.FromSeconds(5)); } catch (Exception exception) { logger.LogDebug(exception, "Could not start Outlook Classic before {OutlookContext}", logContext); } } } internal sealed class OutlookCalendarSession : IDisposable { private readonly object? application; private readonly object? session; private readonly object? calendar; public OutlookCalendarSession( object? application, object? session, object? calendar, object items) { this.application = application; this.session = session; this.calendar = calendar; Items = items; } public object Items { get; } public void Dispose() { OutlookClassicCom.ReleaseComObject(Items); OutlookClassicCom.ReleaseComObject(calendar); OutlookClassicCom.ReleaseComObject(session); OutlookClassicCom.ReleaseComObject(application); } } #endif