using System; using System.Collections.Generic; using System.Linq; using System.Web; using Google.Apis.Calendar.v3.Data; using Google.Apis.Gmail.v1.Data; using MimeKit; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Misc; using Wino.Services; using Wino.Services.Extensions; namespace Wino.Core.Extensions; public static class GoogleIntegratorExtensions { private static string GetNormalizedLabelName(string labelName) { // 1. Remove CATEGORY_ prefix. var normalizedLabelName = labelName.Replace(ServiceConstants.CATEGORY_PREFIX, string.Empty); // 2. Normalize label name by capitalizing first letter. normalizedLabelName = char.ToUpper(normalizedLabelName[0]) + normalizedLabelName.Substring(1).ToLower(); return normalizedLabelName; } public static MailItemFolder GetLocalFolder(this Label label, ListLabelsResponse labelsResponse, Guid accountId) { var normalizedLabelName = GetFolderName(label.Name); // Even though we normalize the label name, check is done by capitalizing the label name. var capitalNormalizedLabelName = normalizedLabelName.ToUpper(); bool isSpecialFolder = ServiceConstants.KnownFolderDictionary.ContainsKey(capitalNormalizedLabelName); var specialFolderType = isSpecialFolder ? ServiceConstants.KnownFolderDictionary[capitalNormalizedLabelName] : SpecialFolderType.Other; // We used to support FOLDER_HIDE_IDENTIFIER to hide invisible folders. // However, a lot of people complained that they don't see their folders after the initial sync // without realizing that they are hidden in Gmail settings. Therefore, it makes more sense to ignore Gmail's configuration // since Wino allows folder visibility configuration separately. // Overridden hidden labels are shown in the UI. // Also Gmail does not support folder sync enable/disable options due to history changes. // By default all folders will be enabled for synchronization. bool isHidden = false; bool isChildOfCategoryFolder = label.Name.StartsWith(ServiceConstants.CATEGORY_PREFIX); bool isSticky = isSpecialFolder && specialFolderType != SpecialFolderType.Category && !isChildOfCategoryFolder; // By default, all special folders update unread count in the UI except Trash. bool shouldShowUnreadCount = specialFolderType != SpecialFolderType.Deleted || specialFolderType != SpecialFolderType.Other; bool isSystemFolder = label.Type == ServiceConstants.SYSTEM_FOLDER_IDENTIFIER; var localFolder = new MailItemFolder() { TextColorHex = label.Color?.TextColor, BackgroundColorHex = label.Color?.BackgroundColor, FolderName = normalizedLabelName, RemoteFolderId = label.Id, Id = Guid.NewGuid(), MailAccountId = accountId, IsSynchronizationEnabled = true, SpecialFolderType = specialFolderType, IsSystemFolder = isSystemFolder, IsSticky = isSticky, IsHidden = isHidden, ShowUnreadCount = shouldShowUnreadCount, }; localFolder.ParentRemoteFolderId = isChildOfCategoryFolder ? string.Empty : GetParentFolderRemoteId(label.Name, labelsResponse); return localFolder; } public static bool GetIsDraft(this Message message) => message?.LabelIds?.Any(a => a == ServiceConstants.DRAFT_LABEL_ID) ?? false; public static bool GetIsUnread(this Message message) => message?.LabelIds?.Any(a => a == ServiceConstants.UNREAD_LABEL_ID) ?? false; public static bool GetIsFocused(this Message message) => message?.LabelIds?.Any(a => a == ServiceConstants.IMPORTANT_LABEL_ID) ?? false; public static bool GetIsFlagged(this Message message) => message?.LabelIds?.Any(a => a == ServiceConstants.STARRED_LABEL_ID) ?? false; private static string GetParentFolderRemoteId(string fullLabelName, ListLabelsResponse labelsResponse) { if (string.IsNullOrEmpty(fullLabelName)) return string.Empty; // Find the last index of '/' int lastIndex = fullLabelName.LastIndexOf('/'); // If '/' not found or it's at the start, return the empty string. if (lastIndex <= 0) return string.Empty; // Extract the parent label var parentLabelName = fullLabelName.Substring(0, lastIndex); return labelsResponse.Labels.FirstOrDefault(a => a.Name == parentLabelName)?.Id ?? string.Empty; } public static string GetFolderName(string fullFolderName) { if (string.IsNullOrEmpty(fullFolderName)) return string.Empty; // Folders with "//" at the end has "/" as the name. if (fullFolderName.EndsWith(ServiceConstants.FOLDER_SEPERATOR_STRING)) return ServiceConstants.FOLDER_SEPERATOR_STRING; string[] parts = fullFolderName.Split(ServiceConstants.FOLDER_SEPERATOR_CHAR); var lastPart = parts[parts.Length - 1]; return GetNormalizedLabelName(lastPart); } /// /// Returns MailCopy out of native Gmail message and converted MimeMessage of that native messaage. /// /// Gmail Message /// MimeMessage representation of that native message. /// MailCopy object that is ready to be inserted to database. public static MailCopy AsMailCopy(this Message gmailMessage, MimeMessage mimeMessage) { bool isUnread = gmailMessage.GetIsUnread(); bool isFocused = gmailMessage.GetIsFocused(); bool isFlagged = gmailMessage.GetIsFlagged(); bool isDraft = gmailMessage.GetIsDraft(); return new MailCopy() { CreationDate = mimeMessage.Date.UtcDateTime, Subject = HttpUtility.HtmlDecode(mimeMessage.Subject), FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage), FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage), PreviewText = HttpUtility.HtmlDecode(gmailMessage.Snippet), ThreadId = gmailMessage.ThreadId, Importance = (MailImportance)mimeMessage.Importance, Id = gmailMessage.Id, IsDraft = isDraft, HasAttachments = mimeMessage.Attachments.Any(), IsRead = !isUnread, IsFlagged = isFlagged, IsFocused = isFocused, InReplyTo = mimeMessage.InReplyTo, MessageId = mimeMessage.MessageId, References = mimeMessage.References.GetReferences(), FileId = Guid.NewGuid() }; } public static List GetRemoteAliases(this ListSendAsResponse response) { return response?.SendAs?.Select(a => new RemoteAccountAlias() { AliasAddress = a.SendAsEmail, IsRootAlias = a.IsDefault.GetValueOrDefault(), IsPrimary = a.IsPrimary.GetValueOrDefault(), ReplyToAddress = a.ReplyToAddress, AliasSenderName = a.DisplayName, IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(), }).ToList(); } public static AccountCalendar AsCalendar(this CalendarListEntry calendarListEntry, Guid accountId) { var calendar = new AccountCalendar() { RemoteCalendarId = calendarListEntry.Id, AccountId = accountId, Name = calendarListEntry.Summary, Id = Guid.NewGuid(), TimeZone = calendarListEntry.TimeZone, IsPrimary = calendarListEntry.Primary.GetValueOrDefault(), }; // Bg color must present. Generate one if doesnt exists. // Text color is optional. It'll be overriden by UI for readibility. calendar.BackgroundColorHex = string.IsNullOrEmpty(calendarListEntry.BackgroundColor) ? ColorHelpers.GenerateFlatColorHex() : calendarListEntry.BackgroundColor; calendar.TextColorHex = string.IsNullOrEmpty(calendarListEntry.ForegroundColor) ? "#000000" : calendarListEntry.ForegroundColor; return calendar; } public static DateTimeOffset GetEventDateTimeOffset(EventDateTime calendarEvent) { if (calendarEvent != null) { if (calendarEvent.DateTimeDateTimeOffset != null) { return calendarEvent.DateTimeDateTimeOffset.Value; } else if (calendarEvent.Date != null) { if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime)) { // Date-only events are treated as UTC midnight return new DateTimeOffset(eventDateTime, TimeSpan.Zero); } else { throw new Exception("Invalid date format in Google Calendar event date."); } } } throw new Exception("Invalid date format in Google Calendar event date."); } /// /// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545. /// /// ___ separated lines. public static string GetRecurrenceString(this Event calendarEvent) { if (calendarEvent == null || calendarEvent.Recurrence == null || !calendarEvent.Recurrence.Any()) { return null; } return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence); } }