using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.WinUI; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using MimeKit; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Helpers; using Wino.Mail.ViewModels.Data; using Wino.Mail.WinUI; namespace Wino.Controls; public sealed partial class CalendarMailItemDisplayInformationControl : UserControl { private static readonly ConcurrentDictionary EventDateRangeCache = []; private readonly IMimeFileService _mimeFileService; private readonly IPreferencesService _preferencesService; private CancellationTokenSource? _loadingCts; [GeneratedDependencyProperty] public partial MailItemViewModel? MailItem { get; set; } [GeneratedDependencyProperty(DefaultValue = MailListDisplayMode.Spacious)] public partial MailListDisplayMode DisplayMode { get; set; } [GeneratedDependencyProperty(DefaultValue = false)] public partial bool Prefer24HourTimeFormat { get; set; } [GeneratedDependencyProperty(DefaultValue = "")] public partial string EventDateRangeText { get; set; } public event EventHandler? HoverActionExecuted; public CalendarMailItemDisplayInformationControl() { InitializeComponent(); _mimeFileService = App.Current.Services.GetRequiredService(); _preferencesService = App.Current.Services.GetRequiredService(); DisplayMode = _preferencesService.MailItemDisplayMode; Prefer24HourTimeFormat = _preferencesService.Prefer24HourTimeFormat; Unloaded += OnControlUnloaded; } partial void OnMailItemPropertyChanged(DependencyPropertyChangedEventArgs e) { _ = LoadEventDateRangeAsync(); } private async Task LoadEventDateRangeAsync() { _loadingCts?.Cancel(); _loadingCts?.Dispose(); _loadingCts = new CancellationTokenSource(); var token = _loadingCts.Token; if (MailItem?.MailCopy == null) { EventDateRangeText = string.Empty; return; } if (EventDateRangeCache.TryGetValue(MailItem.MailCopy.FileId, out var cachedValue)) { EventDateRangeText = cachedValue; return; } try { var accountId = MailItem.MailCopy.AssignedAccount?.Id; if (accountId == null || accountId == Guid.Empty) { EventDateRangeText = Translator.UnknownDateHeader; return; } var isMimeExists = await _mimeFileService.IsMimeExistAsync(accountId.Value, MailItem.MailCopy.FileId); if (!isMimeExists) { EventDateRangeText = Translator.UnknownDateHeader; return; } var mimeInfo = await _mimeFileService.GetMimeMessageInformationAsync(MailItem.MailCopy.FileId, accountId.Value, token); if (mimeInfo == null) { EventDateRangeText = Translator.UnknownDateHeader; return; } var renderedDateRange = ExtractCalendarDateRange(mimeInfo.MimeMessage); EventDateRangeText = string.IsNullOrWhiteSpace(renderedDateRange) ? Translator.UnknownDateHeader : renderedDateRange; EventDateRangeCache.TryAdd(MailItem.MailCopy.FileId, EventDateRangeText); } catch (OperationCanceledException) { // Ignore; newer bind request superseded this one. } catch { EventDateRangeText = Translator.UnknownDateHeader; } } private void BaseMailControlHoverActionExecuted(object sender, MailOperationPreperationRequest e) => HoverActionExecuted?.Invoke(this, e); private string ExtractCalendarDateRange(MimeMessage message) { var calendarContent = GetCalendarContent(message); if (string.IsNullOrWhiteSpace(calendarContent)) { return string.Empty; } var unfoldedIcs = UnfoldIcs(calendarContent); var eventSection = ExtractFirstVEventSection(unfoldedIcs); if (!TryReadIcsDateValue(eventSection, "DTSTART", out var dtStartValue, out var dtStartTzId)) { return string.Empty; } if (!TryParseIcsDateTime(dtStartValue, dtStartTzId, out var startLocal, out var isAllDay)) { return string.Empty; } DateTime endLocal = startLocal; if (TryReadIcsDateValue(eventSection, "DTEND", out var dtEndValue, out var dtEndTzId)) { if (!TryParseIcsDateTime(dtEndValue, dtEndTzId, out endLocal, out var endIsAllDay)) { endLocal = startLocal; } isAllDay = isAllDay && endIsAllDay; } return FormatDisplayDateRange(startLocal, endLocal, isAllDay); } private static string GetCalendarContent(MimeMessage message) { var calendarTextPart = message.BodyParts .OfType() .FirstOrDefault(x => x.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true); if (calendarTextPart != null) { return calendarTextPart.Text ?? string.Empty; } var calendarMimePart = message.BodyParts .OfType() .FirstOrDefault(x => x.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true); if (calendarMimePart == null) { return string.Empty; } using var stream = new MemoryStream(); calendarMimePart.Content.DecodeTo(stream); var contentBytes = stream.ToArray(); if (contentBytes.Length == 0) { return string.Empty; } var charset = calendarMimePart.ContentType?.Charset; var encoding = string.IsNullOrWhiteSpace(charset) ? System.Text.Encoding.UTF8 : System.Text.Encoding.GetEncoding(charset); return encoding.GetString(contentBytes); } private static string UnfoldIcs(string content) => content .Replace("\r\n ", string.Empty, StringComparison.Ordinal) .Replace("\r\n\t", string.Empty, StringComparison.Ordinal) .Replace("\n ", string.Empty, StringComparison.Ordinal) .Replace("\n\t", string.Empty, StringComparison.Ordinal); private static string ExtractFirstVEventSection(string ics) { if (string.IsNullOrWhiteSpace(ics)) { return string.Empty; } const string beginVevent = "BEGIN:VEVENT"; const string endVevent = "END:VEVENT"; var beginIndex = ics.IndexOf(beginVevent, StringComparison.OrdinalIgnoreCase); if (beginIndex < 0) { return ics; } var endIndex = ics.IndexOf(endVevent, beginIndex, StringComparison.OrdinalIgnoreCase); if (endIndex < 0) { return ics[beginIndex..]; } return ics.Substring(beginIndex, endIndex - beginIndex + endVevent.Length); } private static bool TryReadIcsDateValue(string ics, string propertyName, out string value, out string timeZoneId) { value = string.Empty; timeZoneId = string.Empty; var lines = ics.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries); foreach (var rawLine in lines) { var line = rawLine.Trim(); if (!line.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase)) { continue; } var colonIndex = line.IndexOf(':'); if (colonIndex <= 0 || colonIndex == line.Length - 1) { continue; } var parameterSection = line[..colonIndex]; value = line[(colonIndex + 1)..].Trim(); var paramsSplit = parameterSection.Split(';', StringSplitOptions.RemoveEmptyEntries); foreach (var parameter in paramsSplit.Skip(1)) { var eqIndex = parameter.IndexOf('='); if (eqIndex <= 0 || eqIndex == parameter.Length - 1) { continue; } var name = parameter[..eqIndex].Trim(); if (!name.Equals("TZID", StringComparison.OrdinalIgnoreCase)) { continue; } timeZoneId = parameter[(eqIndex + 1)..].Trim().Trim('"'); break; } return true; } return false; } private static bool TryParseIcsDateTime(string rawValue, string timeZoneId, out DateTime localDateTime, out bool isAllDay) { localDateTime = default; isAllDay = false; if (string.IsNullOrWhiteSpace(rawValue)) { return false; } var value = rawValue.Trim(); if (value.Length == 8 && DateTime.TryParseExact(value, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOnly)) { localDateTime = dateOnly.Date; isAllDay = true; return true; } var isUtc = value.EndsWith("Z", StringComparison.OrdinalIgnoreCase); if (isUtc) { value = value[..^1]; } var formats = new[] { "yyyyMMdd'T'HHmmss", "yyyyMMdd'T'HHmm" }; if (!DateTime.TryParseExact(value, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate)) { return false; } if (isUtc) { localDateTime = DateTime.SpecifyKind(parsedDate, DateTimeKind.Utc).ToLocalTime(); return true; } if (!string.IsNullOrWhiteSpace(timeZoneId) && TryFindTimeZone(timeZoneId, out var sourceZone)) { var unspecifiedTime = DateTime.SpecifyKind(parsedDate, DateTimeKind.Unspecified); localDateTime = TimeZoneInfo.ConvertTime(unspecifiedTime, sourceZone, TimeZoneInfo.Local); return true; } localDateTime = DateTime.SpecifyKind(parsedDate, DateTimeKind.Local); return true; } private static bool TryFindTimeZone(string timeZoneId, out TimeZoneInfo timeZone) { timeZone = null!; try { timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); return true; } catch { if (TimeZoneInfo.TryConvertIanaIdToWindowsId(timeZoneId, out var windowsId)) { try { timeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsId); return true; } catch { return false; } } } return false; } private string FormatDisplayDateRange(DateTime startLocal, DateTime endLocal, bool isAllDay) { if (endLocal < startLocal) { endLocal = startLocal; } var culture = CultureInfo.DefaultThreadCurrentUICulture; if (isAllDay) { var adjustedEnd = endLocal.Date > startLocal.Date ? endLocal.Date.AddDays(-1) : startLocal.Date; if (adjustedEnd.Date > startLocal.Date) { return $"{startLocal.ToString("d", culture)} - {adjustedEnd.ToString("d", culture)} ({Translator.CalendarItemAllDay})"; } return $"{startLocal.ToString("d", culture)} ({Translator.CalendarItemAllDay})"; } var timeFormat = Prefer24HourTimeFormat ? "HH:mm" : "h:mm tt"; if (startLocal.Date == endLocal.Date) { return $"{startLocal.ToString($"ddd, MMM d {timeFormat}", culture)} - {endLocal.ToString(timeFormat, culture)}"; } return $"{startLocal.ToString($"ddd, MMM d {timeFormat}", culture)} - {endLocal.ToString($"ddd, MMM d {timeFormat}", culture)}"; } private void OnControlUnloaded(object sender, RoutedEventArgs e) { _loadingCts?.Cancel(); } }