Cleaning up the solution. Separating Shared.WinRT, Services and Synchronization. Removing synchronization from app. Reducing bundle size by 45mb.

This commit is contained in:
Burak Kaan Köse
2024-07-21 05:45:02 +02:00
parent f112f369a7
commit 495885e006
523 changed files with 2254 additions and 2375 deletions

View File

@@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using Google.Apis.Gmail.v1.Data;
using MimeKit;
using MimeKit.IO;
using MimeKit.IO.Filters;
using Wino.Domain.Entities;
using Wino.Domain.Enums;
using Constants = Wino.Domain.Constants;
namespace Wino.Core.Extensions
{
public static class GoogleIntegratorExtensions
{
public const string INBOX_LABEL_ID = "INBOX";
public const string UNREAD_LABEL_ID = "UNREAD";
public const string IMPORTANT_LABEL_ID = "IMPORTANT";
public const string STARRED_LABEL_ID = "STARRED";
public const string DRAFT_LABEL_ID = "DRAFT";
public const string SENT_LABEL_ID = "SENT";
public const string SPAM_LABEL_ID = "SPAM";
public const string CHAT_LABEL_ID = "CHAT";
public const string TRASH_LABEL_ID = "TRASH";
// Label visibility identifiers.
private const string SYSTEM_FOLDER_IDENTIFIER = "system";
private const string FOLDER_HIDE_IDENTIFIER = "labelHide";
private const string CATEGORY_PREFIX = "CATEGORY_";
private const string FOLDER_SEPERATOR_STRING = "/";
private const char FOLDER_SEPERATOR_CHAR = '/';
private static Dictionary<string, SpecialFolderType> KnownFolderDictionary = new Dictionary<string, SpecialFolderType>()
{
{ INBOX_LABEL_ID, SpecialFolderType.Inbox },
{ CHAT_LABEL_ID, SpecialFolderType.Chat },
{ IMPORTANT_LABEL_ID, SpecialFolderType.Important },
{ TRASH_LABEL_ID, SpecialFolderType.Deleted },
{ DRAFT_LABEL_ID, SpecialFolderType.Draft },
{ SENT_LABEL_ID, SpecialFolderType.Sent },
{ SPAM_LABEL_ID, SpecialFolderType.Junk },
{ STARRED_LABEL_ID, SpecialFolderType.Starred },
{ UNREAD_LABEL_ID, SpecialFolderType.Unread },
{ Constants.FORUMS_LABEL_ID, SpecialFolderType.Forums },
{ Constants.UPDATES_LABEL_ID, SpecialFolderType.Updates },
{ Constants.PROMOTIONS_LABEL_ID, SpecialFolderType.Promotions },
{ Constants.SOCIAL_LABEL_ID, SpecialFolderType.Social},
{ Constants.PERSONAL_LABEL_ID, SpecialFolderType.Personal},
};
private static string GetNormalizedLabelName(string labelName)
{
// 1. Remove CATEGORY_ prefix.
var normalizedLabelName = labelName.Replace(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)
{
bool isAllCapital = label.Name.All(a => char.IsUpper(a));
var normalizedLabelName = GetFolderName(label);
// Even though we normalize the label name, check is done by capitalizing the label name.
var capitalNormalizedLabelName = normalizedLabelName.ToUpper();
bool isSpecialFolder = KnownFolderDictionary.ContainsKey(capitalNormalizedLabelName);
var specialFolderType = isSpecialFolder ? 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, but they have their synchronization disabled.
// This is mainly because 'All Mails' label is hidden by default in Gmail, but there is no point to download all mails.
bool shouldEnableSynchronization = label.LabelListVisibility != FOLDER_HIDE_IDENTIFIER;
bool isHidden = false;
bool isChildOfCategoryFolder = label.Name.StartsWith(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 == 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 = shouldEnableSynchronization,
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 == DRAFT_LABEL_ID) ?? false;
public static bool GetIsUnread(this Message message)
=> message?.LabelIds?.Any(a => a == UNREAD_LABEL_ID) ?? false;
public static bool GetIsFocused(this Message message)
=> message?.LabelIds?.Any(a => a == IMPORTANT_LABEL_ID) ?? false;
public static bool GetIsFlagged(this Message message)
=> message?.LabelIds?.Any(a => a == 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(Label label)
{
if (string.IsNullOrEmpty(label.Name)) return string.Empty;
// Folders with "//" at the end has "/" as the name.
if (label.Name.EndsWith(FOLDER_SEPERATOR_STRING)) return FOLDER_SEPERATOR_STRING;
string[] parts = label.Name.Split(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 Tuple<MailCopy, MimeMessage, IEnumerable<string>> GetMailDetails(this Message message)
{
MimeMessage mimeMessage = message.GetGmailMimeMessage();
if (mimeMessage == null)
{
// This should never happen.
Debugger.Break();
return default;
}
bool isUnread = message.GetIsUnread();
bool isFocused = message.GetIsFocused();
bool isFlagged = message.GetIsFlagged();
bool isDraft = message.GetIsDraft();
var mailCopy = new MailCopy()
{
CreationDate = mimeMessage.Date.UtcDateTime,
Subject = HttpUtility.HtmlDecode(mimeMessage.Subject),
FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage),
FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage),
PreviewText = HttpUtility.HtmlDecode(message.Snippet),
ThreadId = message.ThreadId,
Importance = (MailImportance)mimeMessage.Importance,
Id = message.Id,
IsDraft = isDraft,
HasAttachments = mimeMessage.Attachments.Any(),
IsRead = !isUnread,
IsFlagged = isFlagged,
IsFocused = isFocused,
InReplyTo = mimeMessage.InReplyTo,
MessageId = mimeMessage.MessageId,
References = mimeMessage.References.GetReferences()
};
return new Tuple<MailCopy, MimeMessage, IEnumerable<string>>(mailCopy, mimeMessage, message.LabelIds);
}
/// <summary>
/// Returns MimeKit.MimeMessage instance for this GMail Message's Raw content.
/// </summary>
/// <param name="message">GMail message.</param>
public static MimeMessage GetGmailMimeMessage(this Message message)
{
if (message == null || message.Raw == null)
return null;
// Gmail raw is not base64 but base64Safe. We need to remove this HTML things.
var base64Encoded = message.Raw.Replace(",", "=").Replace("-", "+").Replace("_", "/");
byte[] bytes = Encoding.ASCII.GetBytes(base64Encoded);
var stream = new MemoryStream(bytes);
// This method will dispose outer stream.
using (stream)
{
using var filtered = new FilteredStream(stream);
filtered.Add(DecoderFilter.Create(ContentEncoding.Base64));
return MimeMessage.Load(filtered);
}
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Wino.Domain.Enums;
using Wino.Domain.Interfaces;
namespace Wino.Core.Extensions
{
public static class ListExtensions
{
public static IEnumerable<T> FlattenBy<T>(this IEnumerable<T> nodes, Func<T, IEnumerable<T>> selector)
{
if (nodes.Any() == false)
return nodes;
var descendants = nodes
.SelectMany(selector)
.FlattenBy(selector);
return nodes.Concat(descendants);
}
public static IEnumerable<IBatchChangeRequest> CreateBatch(this IEnumerable<IGrouping<MailSynchronizerOperation, IRequestBase>> items)
{
IBatchChangeRequest batch = null;
foreach (var group in items)
{
var key = group.Key;
}
yield return batch;
}
public static void AddSorted<T>(this List<T> @this, T item) where T : IComparable<T>
{
if (@this.Count == 0)
{
@this.Add(item);
return;
}
if (@this[@this.Count - 1].CompareTo(item) <= 0)
{
@this.Add(item);
return;
}
if (@this[0].CompareTo(item) >= 0)
{
@this.Insert(0, item);
return;
}
int index = @this.BinarySearch(item);
if (index < 0)
index = ~index;
@this.Insert(index, item);
}
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Linq;
using MailKit;
using MimeKit;
using Wino.Domain;
using Wino.Domain;
using Wino.Domain.Entities;
using Wino.Domain.Enums;
using Wino.Services.Extensions;
namespace Wino.Core.Extensions
{
public static class MailkitClientExtensions
{
public static string CreateUid(Guid folderId, uint messageUid)
=> $"{folderId}{Constants.MailCopyUidSeparator}{messageUid}";
public static MailImportance GetImportance(this MimeMessage messageSummary)
{
if (messageSummary.Headers != null && messageSummary.Headers.Contains(HeaderId.Importance))
{
var rawImportance = messageSummary.Headers[HeaderId.Importance];
return rawImportance switch
{
"Low" => MailImportance.Low,
"High" => MailImportance.High,
_ => MailImportance.Normal,
};
}
return MailImportance.Normal;
}
public static bool GetIsRead(this MessageFlags? flags)
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Seen);
public static bool GetIsFlagged(this MessageFlags? flags)
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Flagged);
public static string GetThreadId(this IMessageSummary messageSummary)
{
// First check whether we have the default values.
if (!string.IsNullOrEmpty(messageSummary.ThreadId))
return messageSummary.ThreadId;
if (messageSummary.GMailThreadId != null)
return messageSummary.GMailThreadId.ToString();
return default;
}
public static string GetMessageId(this MimeMessage mimeMessage)
=> mimeMessage.MessageId;
public static string GetReferences(this MessageIdList messageIdList)
=> string.Join(";", messageIdList);
public static string GetInReplyTo(this MimeMessage mimeMessage)
{
if (mimeMessage.Headers.Contains(HeaderId.InReplyTo))
{
// Normalize if <> brackets are there.
var inReplyTo = mimeMessage.Headers[HeaderId.InReplyTo];
if (inReplyTo.StartsWith("<") && inReplyTo.EndsWith(">"))
return inReplyTo.Substring(1, inReplyTo.Length - 2);
return inReplyTo;
}
return string.Empty;
}
private static string GetPreviewText(this MimeMessage message)
{
if (string.IsNullOrEmpty(message.HtmlBody))
return message.TextBody;
else
return HtmlAgilityPackExtensions.GetPreviewText(message.HtmlBody);
}
public static MailCopy GetMailDetails(this IMessageSummary messageSummary, MailItemFolder folder, MimeMessage mime)
{
// MessageSummary will only have UniqueId, Flags, ThreadId.
// Other properties are extracted directly from the MimeMessage.
// IMAP doesn't have unique id for mails.
// All mails are mapped to specific folders with incremental Id.
// Uid 1 may belong to different messages in different folders, but can never be
// same for different messages in same folders.
// Here we create arbitrary Id that maps the Id of the message with Folder UniqueId.
// When folder becomes invalid, we'll clear out these MailCopies as well.
var messageUid = CreateUid(folder.Id, messageSummary.UniqueId.Id);
var previewText = mime.GetPreviewText();
var copy = new MailCopy()
{
Id = messageUid,
CreationDate = mime.Date.UtcDateTime,
ThreadId = messageSummary.GetThreadId(),
MessageId = mime.GetMessageId(),
Subject = mime.Subject,
IsRead = messageSummary.Flags.GetIsRead(),
IsFlagged = messageSummary.Flags.GetIsFlagged(),
PreviewText = previewText,
FromAddress = GetActualSenderAddress(mime),
FromName = GetActualSenderName(mime),
IsFocused = false,
Importance = mime.GetImportance(),
References = mime.References?.GetReferences(),
InReplyTo = mime.GetInReplyTo(),
HasAttachments = mime.Attachments.Any(),
FileId = Guid.NewGuid()
};
return copy;
}
// TODO: Name and Address parsing should be handled better.
// At some point Wino needs better contact management.
public static string GetActualSenderName(MimeMessage message)
{
if (message == null)
return string.Empty;
// From MimeKit
// The "From" header specifies the author(s) of the message.
// If more than one MimeKit.MailboxAddress is added to the list of "From" addresses,
// the MimeKit.MimeMessage.Sender should be set to the single MimeKit.MailboxAddress
// of the personal actually sending the message.
// Also handle: https://stackoverflow.com/questions/46474030/mailkit-from-address
if (message.Sender != null)
return string.IsNullOrEmpty(message.Sender.Name) ? message.Sender.Address : message.Sender.Name;
else if (message.From?.Mailboxes != null)
{
var firstAvailableName = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Name))?.Name;
if (string.IsNullOrEmpty(firstAvailableName))
{
var firstAvailableAddress = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Address))?.Address;
if (!string.IsNullOrEmpty(firstAvailableAddress))
{
return firstAvailableAddress;
}
}
return firstAvailableName;
}
// No sender, no from, I don't know what to do.
return Translator.UnknownSender;
}
// TODO: This is wrong.
public static string GetActualSenderAddress(MimeMessage mime)
{
if (mime == null)
return string.Empty;
bool hasSingleFromMailbox = mime.From.Mailboxes.Count() == 1;
if (hasSingleFromMailbox)
return mime.From.Mailboxes.First().GetAddress(idnEncode: true);
else if (mime.Sender != null)
return mime.Sender.GetAddress(idnEncode: true);
else
return Translator.UnknownSender;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using MailKit;
using Wino.Domain.Entities;
using Wino.Domain.Enums;
namespace Wino.Core.Extensions
{
public static class MailkitExtensions
{
public static MailItemFolder GetLocalFolder(this IMailFolder mailkitMailFolder)
{
return new MailItemFolder()
{
Id = Guid.NewGuid(),
FolderName = mailkitMailFolder.Name,
RemoteFolderId = mailkitMailFolder.FullName,
ParentRemoteFolderId = mailkitMailFolder.ParentFolder?.FullName,
SpecialFolderType = SpecialFolderType.Other
};
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
using Microsoft.Graph.Models;
using Wino.Domain.Entities;
using Wino.Domain.Enums;
namespace Wino.Core.Extensions
{
public static class OutlookIntegratorExtensions
{
public static MailItemFolder GetLocalFolder(this MailFolder nativeFolder, Guid accountId)
{
return new MailItemFolder()
{
Id = Guid.NewGuid(),
FolderName = nativeFolder.DisplayName,
RemoteFolderId = nativeFolder.Id,
ParentRemoteFolderId = nativeFolder.ParentFolderId,
IsSynchronizationEnabled = true,
MailAccountId = accountId,
IsHidden = nativeFolder.IsHidden.GetValueOrDefault()
};
}
public static bool GetIsDraft(this Message message)
=> message != null && message.IsDraft.GetValueOrDefault();
public static bool GetIsRead(this Message message)
=> message != null && message.IsRead.GetValueOrDefault();
public static bool GetIsFocused(this Message message)
=> message?.InferenceClassification != null && message.InferenceClassification.Value == InferenceClassificationType.Focused;
public static bool GetIsFlagged(this Message message)
=> message?.Flag?.FlagStatus != null && message.Flag.FlagStatus == FollowupFlagStatus.Flagged;
public static MailCopy AsMailCopy(this Message outlookMessage)
{
bool isDraft = GetIsDraft(outlookMessage);
var mailCopy = new MailCopy()
{
MessageId = outlookMessage.InternetMessageId,
IsFlagged = GetIsFlagged(outlookMessage),
IsFocused = GetIsFocused(outlookMessage),
Importance = !outlookMessage.Importance.HasValue ? MailImportance.Normal : (MailImportance)outlookMessage.Importance.Value,
IsRead = GetIsRead(outlookMessage),
IsDraft = isDraft,
CreationDate = outlookMessage.ReceivedDateTime.GetValueOrDefault().DateTime,
HasAttachments = outlookMessage.HasAttachments.GetValueOrDefault(),
PreviewText = outlookMessage.BodyPreview,
Id = outlookMessage.Id,
ThreadId = outlookMessage.ConversationId,
FromName = outlookMessage.From?.EmailAddress?.Name,
FromAddress = outlookMessage.From?.EmailAddress?.Address,
Subject = outlookMessage.Subject,
FileId = Guid.NewGuid()
};
if (mailCopy.IsDraft)
mailCopy.DraftId = mailCopy.ThreadId;
return mailCopy;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Wino.Core.Extensions
{
public static class StringExtensions
{
public static bool Contains(this string source, string toCheck, StringComparison comp)
{
return source?.IndexOf(toCheck, comp) >= 0;
}
public static string ReplaceFirst(this string text, string search, string replace)
{
int pos = text.IndexOf(search);
if (pos < 0)
{
return text;
}
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
}
}
}