232 lines
9.5 KiB
C#
232 lines
9.5 KiB
C#
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns MailCopy out of native Gmail message and converted MimeMessage of that native messaage.
|
|
/// </summary>
|
|
/// <param name="gmailMessage">Gmail Message</param>
|
|
/// <param name="mimeMessage">MimeMessage representation of that native message.</param>
|
|
/// <returns>MailCopy object that is ready to be inserted to database.</returns>
|
|
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<RemoteAccountAlias> 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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
|
|
/// </summary>
|
|
/// <returns>___ separated lines.</returns>
|
|
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);
|
|
}
|
|
}
|