Separation of core library from the UWP app.

This commit is contained in:
Burak Kaan Köse
2024-11-30 23:05:07 +01:00
parent 4e25dbf5e3
commit 0cd1568c64
88 changed files with 481 additions and 353 deletions

View File

@@ -6,67 +6,21 @@ using Google.Apis.Gmail.v1.Data;
using MimeKit;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Services;
using Wino.Services.Extensions;
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";
// Category labels.
public const string FORUMS_LABEL_ID = "FORUMS";
public const string UPDATES_LABEL_ID = "UPDATES";
public const string PROMOTIONS_LABEL_ID = "PROMOTIONS";
public const string SOCIAL_LABEL_ID = "SOCIAL";
public const string PERSONAL_LABEL_ID = "PERSONAL";
// 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 },
{ FORUMS_LABEL_ID, SpecialFolderType.Forums },
{ UPDATES_LABEL_ID, SpecialFolderType.Updates },
{ PROMOTIONS_LABEL_ID, SpecialFolderType.Promotions },
{ SOCIAL_LABEL_ID, SpecialFolderType.Social},
{ PERSONAL_LABEL_ID, SpecialFolderType.Personal},
};
public static string[] SubCategoryFolderLabelIds =
[
FORUMS_LABEL_ID,
UPDATES_LABEL_ID,
PROMOTIONS_LABEL_ID,
SOCIAL_LABEL_ID,
PERSONAL_LABEL_ID
];
private static string GetNormalizedLabelName(string labelName)
{
// 1. Remove CATEGORY_ prefix.
var normalizedLabelName = labelName.Replace(CATEGORY_PREFIX, string.Empty);
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();
@@ -81,9 +35,9 @@ namespace Wino.Core.Extensions
// Even though we normalize the label name, check is done by capitalizing the label name.
var capitalNormalizedLabelName = normalizedLabelName.ToUpper();
bool isSpecialFolder = KnownFolderDictionary.ContainsKey(capitalNormalizedLabelName);
bool isSpecialFolder = ServiceConstants.KnownFolderDictionary.ContainsKey(capitalNormalizedLabelName);
var specialFolderType = isSpecialFolder ? KnownFolderDictionary[capitalNormalizedLabelName] : SpecialFolderType.Other;
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
@@ -96,13 +50,13 @@ namespace Wino.Core.Extensions
bool isHidden = false;
bool isChildOfCategoryFolder = label.Name.StartsWith(CATEGORY_PREFIX);
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 == SYSTEM_FOLDER_IDENTIFIER;
bool isSystemFolder = label.Type == ServiceConstants.SYSTEM_FOLDER_IDENTIFIER;
var localFolder = new MailItemFolder()
{
@@ -126,16 +80,16 @@ namespace Wino.Core.Extensions
}
public static bool GetIsDraft(this Message message)
=> message?.LabelIds?.Any(a => a == DRAFT_LABEL_ID) ?? false;
=> message?.LabelIds?.Any(a => a == ServiceConstants.DRAFT_LABEL_ID) ?? false;
public static bool GetIsUnread(this Message message)
=> message?.LabelIds?.Any(a => a == UNREAD_LABEL_ID) ?? false;
=> message?.LabelIds?.Any(a => a == ServiceConstants.UNREAD_LABEL_ID) ?? false;
public static bool GetIsFocused(this Message message)
=> message?.LabelIds?.Any(a => a == IMPORTANT_LABEL_ID) ?? false;
=> message?.LabelIds?.Any(a => a == ServiceConstants.IMPORTANT_LABEL_ID) ?? false;
public static bool GetIsFlagged(this Message message)
=> message?.LabelIds?.Any(a => a == STARRED_LABEL_ID) ?? false;
=> message?.LabelIds?.Any(a => a == ServiceConstants.STARRED_LABEL_ID) ?? false;
private static string GetParentFolderRemoteId(string fullLabelName, ListLabelsResponse labelsResponse)
{
@@ -158,9 +112,9 @@ namespace Wino.Core.Extensions
if (string.IsNullOrEmpty(fullFolderName)) return string.Empty;
// Folders with "//" at the end has "/" as the name.
if (fullFolderName.EndsWith(FOLDER_SEPERATOR_STRING)) return FOLDER_SEPERATOR_STRING;
if (fullFolderName.EndsWith(ServiceConstants.FOLDER_SEPERATOR_STRING)) return ServiceConstants.FOLDER_SEPERATOR_STRING;
string[] parts = fullFolderName.Split(FOLDER_SEPERATOR_CHAR);
string[] parts = fullFolderName.Split(ServiceConstants.FOLDER_SEPERATOR_CHAR);
var lastPart = parts[parts.Length - 1];

View File

@@ -1,121 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using HtmlAgilityPack;
namespace Wino.Core.Extensions
{
public static class HtmlAgilityPackExtensions
{
/// <summary>
/// Clears out the src attribute for all `img` and `v:fill` tags.
/// </summary>
/// <param name="document"></param>
public static void ClearImages(this HtmlDocument document)
{
if (document.DocumentNode.InnerHtml.Contains("<img"))
{
foreach (var eachNode in document.DocumentNode.SelectNodes("//img"))
{
eachNode.Attributes.Remove("src");
}
}
}
/// <summary>
/// Removes `style` tags from the document.
/// </summary>
/// <param name="document"></param>
public static void ClearStyles(this HtmlDocument document)
{
document.DocumentNode
.Descendants()
.Where(n => n.Name.Equals("script", StringComparison.OrdinalIgnoreCase)
|| n.Name.Equals("style", StringComparison.OrdinalIgnoreCase)
|| n.Name.Equals("#comment", StringComparison.OrdinalIgnoreCase))
.ToList()
.ForEach(n => n.Remove());
}
/// <summary>
/// Returns plain text from the HTML content.
/// </summary>
/// <param name="htmlContent">Content to get preview from.</param>
/// <returns>Text body for the html.</returns>
public static string GetPreviewText(string htmlContent)
{
if (string.IsNullOrEmpty(htmlContent)) return string.Empty;
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
StringWriter sw = new StringWriter();
ConvertTo(doc.DocumentNode, sw);
sw.Flush();
return sw.ToString().Replace(Environment.NewLine, "");
}
private static void ConvertContentTo(HtmlNode node, TextWriter outText)
{
foreach (HtmlNode subnode in node.ChildNodes)
{
ConvertTo(subnode, outText);
}
}
private static void ConvertTo(HtmlNode node, TextWriter outText)
{
string html;
switch (node.NodeType)
{
case HtmlNodeType.Comment:
// don't output comments
break;
case HtmlNodeType.Document:
ConvertContentTo(node, outText);
break;
case HtmlNodeType.Text:
// script and style must not be output
string parentName = node.ParentNode.Name;
if ((parentName == "script") || (parentName == "style"))
break;
// get text
html = ((HtmlTextNode)node).Text;
// is it in fact a special closing node output as text?
if (HtmlNode.IsOverlappedClosingElement(html))
break;
// check the text is meaningful and not a bunch of whitespaces
if (html.Trim().Length > 0)
{
outText.Write(HtmlEntity.DeEntitize(html));
}
break;
case HtmlNodeType.Element:
switch (node.Name)
{
case "p":
// treat paragraphs as crlf
outText.Write("\r\n");
break;
case "br":
outText.Write("\r\n");
break;
}
if (node.HasChildNodes)
{
ConvertContentTo(node, outText);
}
break;
}
}
}
}

View File

@@ -1,190 +0,0 @@
using System;
using System.Linq;
using MailKit;
using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Extensions
{
public static class MailkitClientExtensions
{
public static char MailCopyUidSeparator = '_';
public static uint ResolveUid(string mailCopyId)
{
var splitted = mailCopyId.Split(MailCopyUidSeparator);
if (splitted.Length > 1 && uint.TryParse(splitted[1], out uint parsedUint)) return parsedUint;
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
}
public static string CreateUid(Guid folderId, uint messageUid)
=> $"{folderId}{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;
return message.From.Mailboxes.FirstOrDefault()?.Name ?? message.Sender?.Name ?? Translator.UnknownSender;
// 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 message)
{
return message.From.Mailboxes.FirstOrDefault()?.Address ?? message.Sender?.Address ?? Translator.UnknownSender;
//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

@@ -7,6 +7,7 @@ using MimeKit;
using MimeKit.IO;
using MimeKit.IO.Filters;
using MimeKit.Utils;
using Wino.Services.Extensions;
namespace Wino.Core.Extensions
{

View File

@@ -1,15 +0,0 @@
using SqlKata;
using SqlKata.Compilers;
namespace Wino.Core.Extensions
{
public static class SqlKataExtensions
{
private static SqliteCompiler Compiler = new SqliteCompiler();
public static string GetRawQuery(this Query query)
{
return Compiler.Compile(query).ToString();
}
}
}