#if WINDOWS using MeetingAssistant.Calendar; namespace MeetingAssistant.MeetingNotes; internal static class OutlookClassicAppointmentMetadata { public static MeetingMetadata CreateMetadata(object appointment, DateTime end) { var title = ReadSubject(appointment); return new MeetingMetadata( title, ReadAttendees(appointment), ExtractAgenda(Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Body")) ?? ""), OutlookClassicCom.ToLocalOffset(end)); } public static string ReadSubject(object appointment) { var subject = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Subject"))?.Trim(); return string.IsNullOrWhiteSpace(subject) ? "Teams meeting" : subject; } public static bool IsTeamsAppointment(object appointment) { var subject = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Subject")) ?? ""; var location = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Location")) ?? ""; var body = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Body")) ?? ""; return TeamsMeetingMarkerDetector.IsTeamsAppointment(subject, location, body); } public static string ExtractAgenda(string body) { if (string.IsNullOrWhiteSpace(body)) { return ""; } var lines = body.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n').Split('\n'); var agendaLines = new List(); foreach (var line in lines) { if (IsTeamsSeparator(line) || IsTeamsJoinLine(line)) { break; } agendaLines.Add(line); } return string.Join(Environment.NewLine, agendaLines).Trim(); } public static IReadOnlyList ReadAttendees(object appointment) { var attendees = new List(); var organizer = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Organizer")); if (!string.IsNullOrWhiteSpace(organizer)) { attendees.Add(organizer.Trim()); } object? recipients = null; try { recipients = OutlookClassicCom.GetProperty(appointment, "Recipients"); var count = Convert.ToInt32(OutlookClassicCom.GetProperty(recipients!, "Count")); for (var index = 1; index <= count; index++) { object? recipient = null; try { recipient = OutlookClassicCom.Invoke(recipients!, "Item", index); var formatted = FormatRecipient(recipient!); if (!string.IsNullOrWhiteSpace(formatted)) { attendees.Add(formatted); } } finally { OutlookClassicCom.ReleaseComObject(recipient); } } } finally { OutlookClassicCom.ReleaseComObject(recipients); } return NormalizeAttendees(attendees); } public static IReadOnlyList NormalizeAttendees(IEnumerable attendees) { return attendees .Select(NormalizeAttendee) .Where(attendee => !string.IsNullOrWhiteSpace(attendee)) .GroupBy(GetAttendeeDeduplicationKey, StringComparer.OrdinalIgnoreCase) .Select(group => group .OrderByDescending(attendee => attendee.Contains('<', StringComparison.Ordinal)) .ThenBy(attendee => attendee.Length) .First()) .ToList(); } private static bool IsTeamsSeparator(string line) { var trimmed = line.Trim(); return trimmed.Length >= 8 && trimmed.All(character => character is '_' or '-' or '*' or ' '); } private static bool IsTeamsJoinLine(string line) { return TeamsMeetingMarkerDetector.ContainsTeamsMarker(line); } private static string NormalizeAttendee(string attendee) { var normalized = attendee.Trim(); var mailtoPrefix = "mailto:"; normalized = normalized.Replace(mailtoPrefix, "", StringComparison.OrdinalIgnoreCase); while (normalized.Contains(" ", StringComparison.Ordinal)) { normalized = normalized.Replace(" ", " ", StringComparison.Ordinal); } var emailStart = normalized.IndexOf('<', StringComparison.Ordinal); var emailEnd = normalized.LastIndexOf('>'); if (emailStart > 0 && emailEnd > emailStart) { var name = normalized[..emailStart].Trim(); var email = normalized[(emailStart + 1)..emailEnd].Trim(); if (name.Equals(email, StringComparison.OrdinalIgnoreCase)) { return email; } return $"{name} <{email}>"; } return normalized; } private static string GetAttendeeDeduplicationKey(string attendee) { var emailStart = attendee.IndexOf('<', StringComparison.Ordinal); var emailEnd = attendee.LastIndexOf('>'); if (emailStart > 0 && emailEnd > emailStart) { return attendee[(emailStart + 1)..emailEnd].Trim(); } return attendee.Trim(); } private static string FormatRecipient(object recipient) { var name = Convert.ToString(OutlookClassicCom.GetProperty(recipient, "Name"))?.Trim() ?? ""; var email = Convert.ToString(OutlookClassicCom.GetProperty(recipient, "Address"))?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(name)) { return email; } if (string.IsNullOrWhiteSpace(email) || email.Contains('/')) { return name; } return $"{name} <{email}>"; } } #endif