Revert "File scoped namespaces"

This reverts commit d31d8f574e.
This commit is contained in:
Burak Kaan Köse
2025-02-16 11:43:30 +01:00
parent d31d8f574e
commit cf9869b71e
617 changed files with 32097 additions and 31478 deletions

View File

@@ -6,33 +6,34 @@ using Wino.Core.Integration.Processors;
using Wino.Core.Services;
using Wino.Core.Synchronizers.ImapSync;
namespace Wino.Core;
public static class CoreContainerSetup
namespace Wino.Core
{
public static void RegisterCoreServices(this IServiceCollection services)
public static class CoreContainerSetup
{
var loggerLevelSwitcher = new LoggingLevelSwitch();
public static void RegisterCoreServices(this IServiceCollection services)
{
var loggerLevelSwitcher = new LoggingLevelSwitch();
services.AddSingleton(loggerLevelSwitcher);
services.AddSingleton<ISynchronizerFactory, SynchronizerFactory>();
services.AddSingleton(loggerLevelSwitcher);
services.AddSingleton<ISynchronizerFactory, SynchronizerFactory>();
services.AddTransient<IGmailChangeProcessor, GmailChangeProcessor>();
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
services.AddTransient<IOutlookChangeProcessor, OutlookChangeProcessor>();
services.AddTransient<IWinoRequestProcessor, WinoRequestProcessor>();
services.AddTransient<IWinoRequestDelegator, WinoRequestDelegator>();
services.AddTransient<IImapTestService, ImapTestService>();
services.AddTransient<IAuthenticationProvider, AuthenticationProvider>();
services.AddTransient<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddTransient<IFontService, FontService>();
services.AddTransient<IUnsubscriptionService, UnsubscriptionService>();
services.AddTransient<IOutlookAuthenticator, OutlookAuthenticator>();
services.AddTransient<IGmailAuthenticator, GmailAuthenticator>();
services.AddTransient<IGmailChangeProcessor, GmailChangeProcessor>();
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
services.AddTransient<IOutlookChangeProcessor, OutlookChangeProcessor>();
services.AddTransient<IWinoRequestProcessor, WinoRequestProcessor>();
services.AddTransient<IWinoRequestDelegator, WinoRequestDelegator>();
services.AddTransient<IImapTestService, ImapTestService>();
services.AddTransient<IAuthenticationProvider, AuthenticationProvider>();
services.AddTransient<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddTransient<IFontService, FontService>();
services.AddTransient<IUnsubscriptionService, UnsubscriptionService>();
services.AddTransient<IOutlookAuthenticator, OutlookAuthenticator>();
services.AddTransient<IGmailAuthenticator, GmailAuthenticator>();
services.AddTransient<IImapSynchronizationStrategyProvider, ImapSynchronizationStrategyProvider>();
services.AddTransient<CondstoreSynchronizer>();
services.AddTransient<QResyncSynchronizer>();
services.AddTransient<UidBasedSynchronizer>();
services.AddTransient<IImapSynchronizationStrategyProvider, ImapSynchronizationStrategyProvider>();
services.AddTransient<CondstoreSynchronizer>();
services.AddTransient<QResyncSynchronizer>();
services.AddTransient<UidBasedSynchronizer>();
}
}
}

View File

@@ -13,219 +13,220 @@ using Wino.Core.Misc;
using Wino.Services;
using Wino.Services.Extensions;
namespace Wino.Core.Extensions;
public static class GoogleIntegratorExtensions
namespace Wino.Core.Extensions
{
private static string GetNormalizedLabelName(string labelName)
public static class GoogleIntegratorExtensions
{
// 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()
private static string GetNormalizedLabelName(string labelName)
{
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,
};
// 1. Remove CATEGORY_ prefix.
var normalizedLabelName = labelName.Replace(ServiceConstants.CATEGORY_PREFIX, string.Empty);
localFolder.ParentRemoteFolderId = isChildOfCategoryFolder ? string.Empty : GetParentFolderRemoteId(label.Name, labelsResponse);
// 2. Normalize label name by capitalizing first letter.
normalizedLabelName = char.ToUpper(normalizedLabelName[0]) + normalizedLabelName.Substring(1).ToLower();
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.");
}
}
return normalizedLabelName;
}
return null;
}
/// <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())
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.");
}
}
}
return null;
}
return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence);
/// <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);
}
}
}

View File

@@ -2,19 +2,20 @@
using System.Collections.Generic;
using System.Linq;
namespace Wino.Core.Extensions;
public static class ListExtensions
namespace Wino.Core.Extensions
{
public static IEnumerable<T> FlattenBy<T>(this IEnumerable<T> nodes, Func<T, IEnumerable<T>> selector)
public static class ListExtensions
{
if (nodes.Any() == false)
return nodes;
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);
var descendants = nodes
.SelectMany(selector)
.FlattenBy(selector);
return nodes.Concat(descendants);
return nodes.Concat(descendants);
}
}
}

View File

@@ -2,56 +2,57 @@
using System.Collections.Generic;
using System.Text;
namespace Wino.Core.Extensions;
public static class LongExtensions
namespace Wino.Core.Extensions
{
// Returns the human-readable file size for an arbitrary, 64-bit file size
// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
public static string GetBytesReadable(this long i)
public static class LongExtensions
{
// Get absolute value
long absolute_i = (i < 0 ? -i : i);
// Determine the suffix and readable value
string suffix;
double readable;
if (absolute_i >= 0x1000000000000000) // Exabyte
// Returns the human-readable file size for an arbitrary, 64-bit file size
// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
public static string GetBytesReadable(this long i)
{
suffix = "EB";
readable = (i >> 50);
// Get absolute value
long absolute_i = (i < 0 ? -i : i);
// Determine the suffix and readable value
string suffix;
double readable;
if (absolute_i >= 0x1000000000000000) // Exabyte
{
suffix = "EB";
readable = (i >> 50);
}
else if (absolute_i >= 0x4000000000000) // Petabyte
{
suffix = "PB";
readable = (i >> 40);
}
else if (absolute_i >= 0x10000000000) // Terabyte
{
suffix = "TB";
readable = (i >> 30);
}
else if (absolute_i >= 0x40000000) // Gigabyte
{
suffix = "GB";
readable = (i >> 20);
}
else if (absolute_i >= 0x100000) // Megabyte
{
suffix = "MB";
readable = (i >> 10);
}
else if (absolute_i >= 0x400) // Kilobyte
{
suffix = "KB";
readable = i;
}
else
{
return i.ToString("0 B"); // Byte
}
// Divide by 1024 to get fractional value
readable = (readable / 1024);
// Return formatted number with suffix
return readable.ToString("0.# ") + suffix;
}
else if (absolute_i >= 0x4000000000000) // Petabyte
{
suffix = "PB";
readable = (i >> 40);
}
else if (absolute_i >= 0x10000000000) // Terabyte
{
suffix = "TB";
readable = (i >> 30);
}
else if (absolute_i >= 0x40000000) // Gigabyte
{
suffix = "GB";
readable = (i >> 20);
}
else if (absolute_i >= 0x100000) // Megabyte
{
suffix = "MB";
readable = (i >> 10);
}
else if (absolute_i >= 0x400) // Kilobyte
{
suffix = "KB";
readable = i;
}
else
{
return i.ToString("0 B"); // Byte
}
// Divide by 1024 to get fractional value
readable = (readable / 1024);
// Return formatted number with suffix
return readable.ToString("0.# ") + suffix;
}
}

View File

@@ -3,33 +3,34 @@ using System.Linq;
using MailKit;
using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Extensions;
public static class MailkitExtensions
namespace Wino.Core.Extensions
{
public static MailItemFolder GetLocalFolder(this IMailFolder mailkitMailFolder)
public static class MailkitExtensions
{
bool isAllCapital = mailkitMailFolder.Name?.All(a => char.IsUpper(a)) ?? false;
return new MailItemFolder()
public static MailItemFolder GetLocalFolder(this IMailFolder mailkitMailFolder)
{
Id = Guid.NewGuid(),
FolderName = isAllCapital ? mailkitMailFolder.Name.OnlyCapitilizeFirstLetter() : mailkitMailFolder.Name,
RemoteFolderId = mailkitMailFolder.FullName,
ParentRemoteFolderId = mailkitMailFolder.ParentFolder?.FullName,
SpecialFolderType = Domain.Enums.SpecialFolderType.Other
};
}
bool isAllCapital = mailkitMailFolder.Name?.All(a => char.IsUpper(a)) ?? false;
public static string OnlyCapitilizeFirstLetter(this string s)
{
if (string.IsNullOrEmpty(s))
return string.Empty;
return new MailItemFolder()
{
Id = Guid.NewGuid(),
FolderName = isAllCapital ? mailkitMailFolder.Name.OnlyCapitilizeFirstLetter() : mailkitMailFolder.Name,
RemoteFolderId = mailkitMailFolder.FullName,
ParentRemoteFolderId = mailkitMailFolder.ParentFolder?.FullName,
SpecialFolderType = Domain.Enums.SpecialFolderType.Other
};
}
s = s.ToLower();
public static string OnlyCapitilizeFirstLetter(this string s)
{
if (string.IsNullOrEmpty(s))
return string.Empty;
char[] a = s.ToCharArray();
a[0] = char.ToUpper(a[0]);
return new string(a);
s = s.ToLower();
char[] a = s.ToCharArray();
a[0] = char.ToUpper(a[0]);
return new string(a);
}
}
}

View File

@@ -9,101 +9,102 @@ using MimeKit.IO.Filters;
using MimeKit.Utils;
using Wino.Services.Extensions;
namespace Wino.Core.Extensions;
public static class MimeExtensions
namespace Wino.Core.Extensions
{
/// <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)
public static class MimeExtensions
{
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)
/// <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)
{
using var filtered = new FilteredStream(stream);
filtered.Add(DecoderFilter.Create(ContentEncoding.Base64));
if (message == null || message.Raw == null)
return null;
return MimeMessage.Load(filtered);
}
}
// 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);
/// <summary>
/// Sets html body replacing base64 images with cid linked resources.
/// Updates text body based on html.
/// </summary>
/// <param name="bodyBuilder">Body builder.</param>
/// <param name="htmlContent">Html content that can have embedded images.</param>
/// <returns>Body builder with set HtmlBody.</returns>
public static BodyBuilder SetHtmlBody(this BodyBuilder bodyBuilder, string htmlContent)
{
if (string.IsNullOrEmpty(htmlContent)) return bodyBuilder;
var stream = new MemoryStream(bytes);
var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
// This method will dispose outer stream.
var imgNodes = doc.DocumentNode.SelectNodes("//img");
if (imgNodes != null)
{
foreach (var node in imgNodes)
using (stream)
{
var src = node.GetAttributeValue("src", string.Empty);
using var filtered = new FilteredStream(stream);
filtered.Add(DecoderFilter.Create(ContentEncoding.Base64));
if (string.IsNullOrEmpty(src)) continue;
if (!src.StartsWith("data:image"))
{
continue;
}
var parts = src.Substring(11).Split([";base64,"], StringSplitOptions.None);
string mimeType = parts[0];
string base64Content = parts[1];
var alt = node.GetAttributeValue("alt", $"Embedded_Image.{mimeType}");
// Convert the base64 content to binary data
byte[] imageData = Convert.FromBase64String(base64Content);
// Create a new linked resource as MimePart
var image = new MimePart("image", mimeType)
{
ContentId = MimeUtils.GenerateMessageId(),
Content = new MimeContent(new MemoryStream(imageData)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentDescription = alt.Replace(" ", "_"),
FileName = alt,
ContentTransferEncoding = ContentEncoding.Base64
};
bodyBuilder.LinkedResources.Add(image);
node.SetAttributeValue("src", $"cid:{image.ContentId}");
return MimeMessage.Load(filtered);
}
}
bodyBuilder.HtmlBody = doc.DocumentNode.InnerHtml;
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
/// <summary>
/// Sets html body replacing base64 images with cid linked resources.
/// Updates text body based on html.
/// </summary>
/// <param name="bodyBuilder">Body builder.</param>
/// <param name="htmlContent">Html content that can have embedded images.</param>
/// <returns>Body builder with set HtmlBody.</returns>
public static BodyBuilder SetHtmlBody(this BodyBuilder bodyBuilder, string htmlContent)
{
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
}
if (string.IsNullOrEmpty(htmlContent)) return bodyBuilder;
return bodyBuilder;
var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
var imgNodes = doc.DocumentNode.SelectNodes("//img");
if (imgNodes != null)
{
foreach (var node in imgNodes)
{
var src = node.GetAttributeValue("src", string.Empty);
if (string.IsNullOrEmpty(src)) continue;
if (!src.StartsWith("data:image"))
{
continue;
}
var parts = src.Substring(11).Split([";base64,"], StringSplitOptions.None);
string mimeType = parts[0];
string base64Content = parts[1];
var alt = node.GetAttributeValue("alt", $"Embedded_Image.{mimeType}");
// Convert the base64 content to binary data
byte[] imageData = Convert.FromBase64String(base64Content);
// Create a new linked resource as MimePart
var image = new MimePart("image", mimeType)
{
ContentId = MimeUtils.GenerateMessageId(),
Content = new MimeContent(new MemoryStream(imageData)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentDescription = alt.Replace(" ", "_"),
FileName = alt,
ContentTransferEncoding = ContentEncoding.Base64
};
bodyBuilder.LinkedResources.Add(image);
node.SetAttributeValue("src", $"cid:{image.ContentId}");
}
}
bodyBuilder.HtmlBody = doc.DocumentNode.InnerHtml;
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
{
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
}
return bodyBuilder;
}
}
}

View File

@@ -10,351 +10,352 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Misc;
namespace Wino.Core.Extensions;
public static class OutlookIntegratorExtensions
namespace Wino.Core.Extensions
{
public static MailItemFolder GetLocalFolder(this MailFolder nativeFolder, Guid accountId)
public static class OutlookIntegratorExtensions
{
return new MailItemFolder()
public static MailItemFolder GetLocalFolder(this MailFolder nativeFolder, Guid accountId)
{
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;
}
public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders)
{
var fromAddress = GetRecipients(mime.From).ElementAt(0);
var toAddresses = GetRecipients(mime.To).ToList();
var ccAddresses = GetRecipients(mime.Cc).ToList();
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
IsDraft = false,
IsRead = true, // Sent messages are always read.
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = GetProperId(mime.MessageId),
ReplyTo = replyToAddresses,
Attachments = []
};
// Headers are only included when creating the draft.
// When sending, they are not included. Graph will throw an error.
if (includeInternetHeaders)
{
message.InternetMessageHeaders = GetHeaderList(mime);
}
return message;
}
public static AccountCalendar AsCalendar(this Calendar outlookCalendar, MailAccount assignedAccount)
{
var calendar = new AccountCalendar()
{
AccountId = assignedAccount.Id,
Id = Guid.NewGuid(),
RemoteCalendarId = outlookCalendar.Id,
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
Name = outlookCalendar.Name,
IsExtended = true,
};
// Colors:
// Bg must be present. Generate flat one if doesn't exists.
// Text doesnt exists for Outlook.
calendar.BackgroundColorHex = string.IsNullOrEmpty(outlookCalendar.HexColor) ? ColorHelpers.GenerateFlatColorHex() : outlookCalendar.HexColor;
calendar.TextColorHex = "#000000";
return calendar;
}
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeekObject.Monday => "MO",
DayOfWeekObject.Tuesday => "TU",
DayOfWeekObject.Wednesday => "WE",
DayOfWeekObject.Thursday => "TH",
DayOfWeekObject.Friday => "FR",
DayOfWeekObject.Saturday => "SA",
DayOfWeekObject.Sunday => "SU",
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
};
}
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
{
if (recurrence == null || recurrence.Pattern == null)
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
var ruleBuilder = new StringBuilder("RRULE:");
var pattern = recurrence.Pattern;
// Frequency
switch (pattern.Type)
{
case RecurrencePatternType.Daily:
ruleBuilder.Append("FREQ=DAILY;");
break;
case RecurrencePatternType.Weekly:
ruleBuilder.Append("FREQ=WEEKLY;");
break;
case RecurrencePatternType.AbsoluteMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.AbsoluteYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
case RecurrencePatternType.RelativeMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.RelativeYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
default:
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
}
// Interval
if (pattern.Interval > 0)
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
// Days of Week
if (pattern.DaysOfWeek?.Any() == true)
{
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
ruleBuilder.Append($"BYDAY={days};");
}
// Day of Month (BYMONTHDAY)
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
{
if (pattern.DayOfMonth <= 0)
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
}
// Month (BYMONTH)
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
{
if (pattern.Month <= 0)
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
ruleBuilder.Append($"BYMONTH={pattern.Month};");
}
// Count or Until
if (recurrence.Range != null)
{
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
return new MailItemFolder()
{
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
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;
}
public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders)
{
var fromAddress = GetRecipients(mime.From).ElementAt(0);
var toAddresses = GetRecipients(mime.To).ToList();
var ccAddresses = GetRecipients(mime.Cc).ToList();
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
IsDraft = false,
IsRead = true, // Sent messages are always read.
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = GetProperId(mime.MessageId),
ReplyTo = replyToAddresses,
Attachments = []
};
// Headers are only included when creating the draft.
// When sending, they are not included. Graph will throw an error.
if (includeInternetHeaders)
{
message.InternetMessageHeaders = GetHeaderList(mime);
}
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
return message;
}
public static AccountCalendar AsCalendar(this Calendar outlookCalendar, MailAccount assignedAccount)
{
var calendar = new AccountCalendar()
{
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
AccountId = assignedAccount.Id,
Id = Guid.NewGuid(),
RemoteCalendarId = outlookCalendar.Id,
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
Name = outlookCalendar.Name,
IsExtended = true,
};
// Colors:
// Bg must be present. Generate flat one if doesn't exists.
// Text doesnt exists for Outlook.
calendar.BackgroundColorHex = string.IsNullOrEmpty(outlookCalendar.HexColor) ? ColorHelpers.GenerateFlatColorHex() : outlookCalendar.HexColor;
calendar.TextColorHex = "#000000";
return calendar;
}
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeekObject.Monday => "MO",
DayOfWeekObject.Tuesday => "TU",
DayOfWeekObject.Wednesday => "WE",
DayOfWeekObject.Thursday => "TH",
DayOfWeekObject.Friday => "FR",
DayOfWeekObject.Saturday => "SA",
DayOfWeekObject.Sunday => "SU",
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
};
}
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
{
if (recurrence == null || recurrence.Pattern == null)
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
var ruleBuilder = new StringBuilder("RRULE:");
var pattern = recurrence.Pattern;
// Frequency
switch (pattern.Type)
{
case RecurrencePatternType.Daily:
ruleBuilder.Append("FREQ=DAILY;");
break;
case RecurrencePatternType.Weekly:
ruleBuilder.Append("FREQ=WEEKLY;");
break;
case RecurrencePatternType.AbsoluteMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.AbsoluteYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
case RecurrencePatternType.RelativeMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.RelativeYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
default:
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
}
// Interval
if (pattern.Interval > 0)
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
// Days of Week
if (pattern.DaysOfWeek?.Any() == true)
{
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
ruleBuilder.Append($"BYDAY={days};");
}
// Day of Month (BYMONTHDAY)
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
{
if (pattern.DayOfMonth <= 0)
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
}
// Month (BYMONTH)
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
{
if (pattern.Month <= 0)
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
ruleBuilder.Append($"BYMONTH={pattern.Month};");
}
// Count or Until
if (recurrence.Range != null)
{
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
{
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
}
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
{
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
}
}
// Remove trailing semicolon
return ruleBuilder.ToString().TrimEnd(';');
}
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
{
throw new ArgumentException("DateTimeTimeZone is null or empty.");
}
try
{
// Parse the DateTime string
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
else
throw new ArgumentException("DateTime string is not in a valid format.");
}
catch (Exception)
{
throw;
}
}
// Remove trailing semicolon
return ruleBuilder.ToString().TrimEnd(';');
}
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
{
throw new ArgumentException("DateTimeTimeZone is null or empty.");
}
try
{
// Parse the DateTime string
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
return responseType switch
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
else
throw new ArgumentException("DateTime string is not in a valid format.");
ResponseType.None => AttendeeStatus.NeedsAction,
ResponseType.NotResponded => AttendeeStatus.NeedsAction,
ResponseType.Organizer => AttendeeStatus.Accepted,
ResponseType.TentativelyAccepted => AttendeeStatus.Tentative,
ResponseType.Accepted => AttendeeStatus.Accepted,
ResponseType.Declined => AttendeeStatus.Declined,
_ => AttendeeStatus.NeedsAction
};
}
catch (Exception)
public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId)
{
throw;
}
}
bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer;
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
{
return responseType switch
{
ResponseType.None => AttendeeStatus.NeedsAction,
ResponseType.NotResponded => AttendeeStatus.NeedsAction,
ResponseType.Organizer => AttendeeStatus.Accepted,
ResponseType.TentativelyAccepted => AttendeeStatus.Tentative,
ResponseType.Accepted => AttendeeStatus.Accepted,
ResponseType.Declined => AttendeeStatus.Declined,
_ => AttendeeStatus.NeedsAction
};
}
public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId)
{
bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer;
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItemId,
Id = Guid.NewGuid(),
Email = attendee.EmailAddress?.Address,
Name = attendee.EmailAddress?.Name,
AttendenceStatus = GetAttendeeStatus(attendee.Status.Response),
IsOrganizer = isOrganizer,
IsOptionalAttendee = attendee.Type == AttendeeType.Optional,
};
return eventAttendee;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
{
if (address is MailboxAddress mailboxAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = mailboxAddress.Address, Name = mailboxAddress.Name } };
else if (address is GroupAddress groupAddress)
var eventAttendee = new CalendarEventAttendee()
{
// TODO: Group addresses are not directly supported.
// It'll be individually added.
CalendarItemId = calendarItemId,
Id = Guid.NewGuid(),
Email = attendee.EmailAddress?.Address,
Name = attendee.EmailAddress?.Name,
AttendenceStatus = GetAttendeeStatus(attendee.Status.Response),
IsOrganizer = isOrganizer,
IsOptionalAttendee = attendee.Type == AttendeeType.Optional,
};
foreach (var mailbox in groupAddress.Members)
if (mailbox is MailboxAddress groupMemberMailAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = groupMemberMailAddress.Address, Name = groupMemberMailAddress.Name } };
return eventAttendee;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
{
if (address is MailboxAddress mailboxAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = mailboxAddress.Address, Name = mailboxAddress.Name } };
else if (address is GroupAddress groupAddress)
{
// TODO: Group addresses are not directly supported.
// It'll be individually added.
foreach (var mailbox in groupAddress.Members)
if (mailbox is MailboxAddress groupMemberMailAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = groupMemberMailAddress.Address, Name = groupMemberMailAddress.Name } };
}
}
}
}
private static Importance? GetImportance(MessageImportance importance)
{
return importance switch
private static Importance? GetImportance(MessageImportance importance)
{
MessageImportance.Low => Importance.Low,
MessageImportance.Normal => Importance.Normal,
MessageImportance.High => Importance.High,
_ => null
};
}
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
// Here we'll try to ignore some headers that are not neccessary.
// Outlook API will generate them automatically.
// Some headers also require to start with X- or x-.
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>();
int includedHeaderCount = 0;
foreach (var header in mime.Headers)
{
if (!headersToIgnore.Contains(header.Field))
return importance switch
{
var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field;
// No header value should exceed 995 characters.
var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value;
headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue });
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
MessageImportance.Low => Importance.Low,
MessageImportance.Normal => Importance.Normal,
MessageImportance.High => Importance.High,
_ => null
};
}
return headers;
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
// Here we'll try to ignore some headers that are not neccessary.
// Outlook API will generate them automatically.
// Some headers also require to start with X- or x-.
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>();
int includedHeaderCount = 0;
foreach (var header in mime.Headers)
{
if (!headersToIgnore.Contains(header.Field))
{
var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field;
// No header value should exceed 995 characters.
var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value;
headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue });
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
}
return headers;
}
private static string GetProperId(string id)
{
// Outlook requires some identifiers to start with "X-" or "x-".
if (string.IsNullOrEmpty(id)) return string.Empty;
if (!id.StartsWith("x-") || !id.StartsWith("X-"))
return $"X-{id}";
return id;
}
#endregion
}
private static string GetProperId(string id)
{
// Outlook requires some identifiers to start with "X-" or "x-".
if (string.IsNullOrEmpty(id)) return string.Empty;
if (!id.StartsWith("x-") || !id.StartsWith("X-"))
return $"X-{id}";
return id;
}
#endregion
}

View File

@@ -1,21 +1,22 @@
using System;
namespace Wino.Core.Extensions;
public static class StringExtensions
namespace Wino.Core.Extensions
{
public static bool Contains(this string source, string toCheck, StringComparison comp)
public static class StringExtensions
{
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)
public static bool Contains(this string source, string toCheck, StringComparison comp)
{
return text;
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);
}
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
}
}

View File

@@ -5,28 +5,29 @@ using Google.Apis.Http;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Http;
internal class GmailClientMessageHandler : ConfigurableMessageHandler
namespace Wino.Core.Http
{
private readonly IGmailAuthenticator _gmailAuthenticator;
private readonly MailAccount _mailAccount;
public GmailClientMessageHandler(IGmailAuthenticator gmailAuthenticator, MailAccount mailAccount) : base(new HttpClientHandler())
internal class GmailClientMessageHandler : ConfigurableMessageHandler
{
_gmailAuthenticator = gmailAuthenticator;
_mailAccount = mailAccount;
}
private readonly IGmailAuthenticator _gmailAuthenticator;
private readonly MailAccount _mailAccount;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// This call here will automatically trigger Google Auth's interactive login if the token is not found.
// or refresh the token based on the FileDataStore.
public GmailClientMessageHandler(IGmailAuthenticator gmailAuthenticator, MailAccount mailAccount) : base(new HttpClientHandler())
{
_gmailAuthenticator = gmailAuthenticator;
_mailAccount = mailAccount;
}
var tokenInformation = await _gmailAuthenticator.GetTokenInformationAsync(_mailAccount);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// This call here will automatically trigger Google Auth's interactive login if the token is not found.
// or refresh the token based on the FileDataStore.
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInformation.AccessToken);
var tokenInformation = await _gmailAuthenticator.GetTokenInformationAsync(_mailAccount);
return await base.SendAsync(request, cancellationToken);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInformation.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
}

View File

@@ -2,17 +2,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace Wino.Core.Http;
/// <summary>
/// Adds additional Prefer header for immutable id support in the Graph service client.
/// </summary>
public class MicrosoftImmutableIdHandler : DelegatingHandler
namespace Wino.Core.Http
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
/// <summary>
/// Adds additional Prefer header for immutable id support in the Graph service client.
/// </summary>
public class MicrosoftImmutableIdHandler : DelegatingHandler
{
request.Headers.TryAddWithoutValidation("Prefer", "IdType=\"ImmutableId\"");
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.TryAddWithoutValidation("Prefer", "IdType=\"ImmutableId\"");
return base.SendAsync(request, cancellationToken);
return base.SendAsync(request, cancellationToken);
}
}
}

View File

@@ -6,27 +6,28 @@ using Microsoft.Kiota.Abstractions.Authentication;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Http;
public class MicrosoftTokenProvider : IAccessTokenProvider
namespace Wino.Core.Http
{
private readonly MailAccount _account;
private readonly IAuthenticator _authenticator;
public MicrosoftTokenProvider(MailAccount account, IAuthenticator authenticator)
public class MicrosoftTokenProvider : IAccessTokenProvider
{
_account = account;
_authenticator = authenticator;
}
private readonly MailAccount _account;
private readonly IAuthenticator _authenticator;
public AllowedHostsValidator AllowedHostsValidator { get; }
public MicrosoftTokenProvider(MailAccount account, IAuthenticator authenticator)
{
_account = account;
_authenticator = authenticator;
}
public async Task<string> GetAuthorizationTokenAsync(Uri uri,
Dictionary<string, object> additionalAuthenticationContext = null,
CancellationToken cancellationToken = default)
{
var tokenInfo = await _authenticator.GetTokenInformationAsync(_account);
public AllowedHostsValidator AllowedHostsValidator { get; }
return tokenInfo.AccessToken;
public async Task<string> GetAuthorizationTokenAsync(Uri uri,
Dictionary<string, object> additionalAuthenticationContext = null,
CancellationToken cancellationToken = default)
{
var tokenInfo = await _authenticator.GetTokenInformationAsync(_account);
return tokenInfo.AccessToken;
}
}
}

View File

@@ -19,349 +19,350 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Models.Connectivity;
namespace Wino.Core.Integration;
/// <summary>
/// Provides a pooling mechanism for ImapClient.
/// Makes sure that we don't have too many connections to the server.
/// Rents a connected & authenticated client from the pool all the time.
/// </summary>
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
public class ImapClientPool : IDisposable
namespace Wino.Core.Integration
{
// Hardcoded implementation details for ID extension if the server supports.
// Some providers like Chinese 126 require Id to be sent before authentication.
// We don't expose any customer data here. Therefore it's safe for now.
// Later on maybe we can make it configurable and leave it to the user with passing
// real implementation details.
private readonly ImapImplementation _implementation = new()
{
Version = "1.8.0",
OS = "Windows",
Vendor = "Wino",
SupportUrl = "https://www.winomail.app",
Name = "Wino Mail User",
};
public bool ThrowOnSSLHandshakeCallback { get; set; }
public ImapClientPoolOptions ImapClientPoolOptions { get; }
internal WinoImapClient IdleClient { get; set; }
private readonly int MinimumPoolSize = 5;
private readonly ConcurrentStack<IImapClient> _clients = [];
private readonly SemaphoreSlim _semaphore;
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
_customServerInformation = imapClientPoolOptions.ServerInformation;
_protocolLogStream = imapClientPoolOptions.ProtocolLog;
// Set the maximum pool size to 5 or the custom value if it's greater.
_semaphore = new(Math.Max(MinimumPoolSize, _customServerInformation.MaxConcurrentClients));
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
ImapClientPoolOptions = imapClientPoolOptions;
}
/// <summary>
/// Ensures all supported capabilities are enabled in this connection.
/// Reconnects and reauthenticates if necessary.
/// Provides a pooling mechanism for ImapClient.
/// Makes sure that we don't have too many connections to the server.
/// Rents a connected & authenticated client from the pool all the time.
/// </summary>
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew)
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
public class ImapClientPool : IDisposable
{
try
// Hardcoded implementation details for ID extension if the server supports.
// Some providers like Chinese 126 require Id to be sent before authentication.
// We don't expose any customer data here. Therefore it's safe for now.
// Later on maybe we can make it configurable and leave it to the user with passing
// real implementation details.
private readonly ImapImplementation _implementation = new()
{
bool isReconnected = await EnsureConnectedAsync(client);
bool mustDoPostAuthIdentification = false;
if ((isCreatedNew || isReconnected) && client.IsConnected)
{
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
await client.CompressAsync();
// Identify if the server supports ID extension.
// Some servers require it pre-authentication, some post-authentication.
// We'll observe the response here and do it after authentication if needed.
if (client.Capabilities.HasFlag(ImapCapabilities.Id))
{
try
{
await client.IdentifyAsync(_implementation);
}
catch (ImapCommandException commandException) when (commandException.Response == ImapCommandResponse.No || commandException.Response == ImapCommandResponse.Bad)
{
mustDoPostAuthIdentification = true;
}
catch (Exception)
{
throw;
}
}
}
await EnsureAuthenticatedAsync(client);
if ((isCreatedNew || isReconnected) && client.IsAuthenticated)
{
if (mustDoPostAuthIdentification) await client.IdentifyAsync(_implementation);
// Activate post-auth capabilities.
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
{
await client.EnableQuickResyncAsync().ConfigureAwait(false);
if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true;
}
}
}
catch (Exception ex)
{
if (ex.InnerException is ImapTestSSLCertificateException imapTestSSLCertificateException)
throw imapTestSSLCertificateException;
throw new ImapClientPoolException(ex, GetProtocolLogContent());
}
finally
{
// Release it even if it fails.
_semaphore.Release();
}
}
public string GetProtocolLogContent()
{
if (_protocolLogStream == null) return default;
// Set the position to the beginning of the stream in case it is not already at the start
if (_protocolLogStream.CanSeek)
_protocolLogStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true);
return reader.ReadToEnd();
}
public async Task<IImapClient> GetClientAsync()
{
await _semaphore.WaitAsync();
if (_clients.TryPop(out IImapClient item))
{
await EnsureCapabilitiesAsync(item, false);
return item;
}
var client = CreateNewClient();
await EnsureCapabilitiesAsync(client, true);
return client;
}
public void Release(IImapClient item, bool destroyClient = false)
{
if (item != null)
{
if (destroyClient)
{
if (item.IsConnected)
{
lock (item.SyncRoot)
{
item.Disconnect(quit: true);
}
}
_clients.TryPop(out _);
item.Dispose();
}
else if (!_disposedValue)
{
_clients.Push(item);
}
_semaphore.Release();
}
}
private IImapClient CreateNewClient()
{
WinoImapClient client = null;
// Make sure to create a ImapClient with a protocol logger if enabled.
client = _protocolLogStream != null
? new WinoImapClient(new ProtocolLogger(_protocolLogStream))
: new WinoImapClient();
HttpProxyClient proxyClient = null;
// Add proxy client if exists.
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
{
proxyClient = new HttpProxyClient(_customServerInformation.ProxyServer, int.Parse(_customServerInformation.ProxyServerPort));
}
client.ProxyClient = proxyClient;
_logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count);
return client;
}
private SecureSocketOptions GetSocketOptions(ImapConnectionSecurity connectionSecurity)
=> connectionSecurity switch
{
ImapConnectionSecurity.Auto => SecureSocketOptions.Auto,
ImapConnectionSecurity.None => SecureSocketOptions.None,
ImapConnectionSecurity.StartTls => SecureSocketOptions.StartTlsWhenAvailable,
ImapConnectionSecurity.SslTls => SecureSocketOptions.SslOnConnect,
_ => SecureSocketOptions.None
Version = "1.8.0",
OS = "Windows",
Vendor = "Wino",
SupportUrl = "https://www.winomail.app",
Name = "Wino Mail User",
};
/// <returns>True if the connection is newly established.</returns>
public async Task<bool> EnsureConnectedAsync(IImapClient client)
{
if (client.IsConnected) return false;
public bool ThrowOnSSLHandshakeCallback { get; set; }
public ImapClientPoolOptions ImapClientPoolOptions { get; }
internal WinoImapClient IdleClient { get; set; }
client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback;
private readonly int MinimumPoolSize = 5;
await client.ConnectAsync(_customServerInformation.IncomingServer,
int.Parse(_customServerInformation.IncomingServerPort),
GetSocketOptions(_customServerInformation.IncomingServerSocketOption));
private readonly ConcurrentStack<IImapClient> _clients = [];
private readonly SemaphoreSlim _semaphore;
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
// Print out useful information for testing.
if (client.IsConnected && ImapClientPoolOptions.IsTestPool)
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
// Print supported authentication methods for the client.
var supportedAuthMethods = client.AuthenticationMechanisms;
_customServerInformation = imapClientPoolOptions.ServerInformation;
_protocolLogStream = imapClientPoolOptions.ProtocolLog;
if (supportedAuthMethods == null || supportedAuthMethods.Count == 0)
// Set the maximum pool size to 5 or the custom value if it's greater.
_semaphore = new(Math.Max(MinimumPoolSize, _customServerInformation.MaxConcurrentClients));
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
ImapClientPoolOptions = imapClientPoolOptions;
}
/// <summary>
/// Ensures all supported capabilities are enabled in this connection.
/// Reconnects and reauthenticates if necessary.
/// </summary>
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew)
{
try
{
WriteToProtocolLog("There are no supported authentication mechanisms...");
bool isReconnected = await EnsureConnectedAsync(client);
bool mustDoPostAuthIdentification = false;
if ((isCreatedNew || isReconnected) && client.IsConnected)
{
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
await client.CompressAsync();
// Identify if the server supports ID extension.
// Some servers require it pre-authentication, some post-authentication.
// We'll observe the response here and do it after authentication if needed.
if (client.Capabilities.HasFlag(ImapCapabilities.Id))
{
try
{
await client.IdentifyAsync(_implementation);
}
catch (ImapCommandException commandException) when (commandException.Response == ImapCommandResponse.No || commandException.Response == ImapCommandResponse.Bad)
{
mustDoPostAuthIdentification = true;
}
catch (Exception)
{
throw;
}
}
}
await EnsureAuthenticatedAsync(client);
if ((isCreatedNew || isReconnected) && client.IsAuthenticated)
{
if (mustDoPostAuthIdentification) await client.IdentifyAsync(_implementation);
// Activate post-auth capabilities.
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
{
await client.EnableQuickResyncAsync().ConfigureAwait(false);
if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true;
}
}
}
catch (Exception ex)
{
if (ex.InnerException is ImapTestSSLCertificateException imapTestSSLCertificateException)
throw imapTestSSLCertificateException;
throw new ImapClientPoolException(ex, GetProtocolLogContent());
}
finally
{
// Release it even if it fails.
_semaphore.Release();
}
}
public string GetProtocolLogContent()
{
if (_protocolLogStream == null) return default;
// Set the position to the beginning of the stream in case it is not already at the start
if (_protocolLogStream.CanSeek)
_protocolLogStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true);
return reader.ReadToEnd();
}
public async Task<IImapClient> GetClientAsync()
{
await _semaphore.WaitAsync();
if (_clients.TryPop(out IImapClient item))
{
await EnsureCapabilitiesAsync(item, false);
return item;
}
var client = CreateNewClient();
await EnsureCapabilitiesAsync(client, true);
return client;
}
public void Release(IImapClient item, bool destroyClient = false)
{
if (item != null)
{
if (destroyClient)
{
if (item.IsConnected)
{
lock (item.SyncRoot)
{
item.Disconnect(quit: true);
}
}
_clients.TryPop(out _);
item.Dispose();
}
else if (!_disposedValue)
{
_clients.Push(item);
}
_semaphore.Release();
}
}
private IImapClient CreateNewClient()
{
WinoImapClient client = null;
// Make sure to create a ImapClient with a protocol logger if enabled.
client = _protocolLogStream != null
? new WinoImapClient(new ProtocolLogger(_protocolLogStream))
: new WinoImapClient();
HttpProxyClient proxyClient = null;
// Add proxy client if exists.
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
{
proxyClient = new HttpProxyClient(_customServerInformation.ProxyServer, int.Parse(_customServerInformation.ProxyServerPort));
}
client.ProxyClient = proxyClient;
_logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count);
return client;
}
private SecureSocketOptions GetSocketOptions(ImapConnectionSecurity connectionSecurity)
=> connectionSecurity switch
{
ImapConnectionSecurity.Auto => SecureSocketOptions.Auto,
ImapConnectionSecurity.None => SecureSocketOptions.None,
ImapConnectionSecurity.StartTls => SecureSocketOptions.StartTlsWhenAvailable,
ImapConnectionSecurity.SslTls => SecureSocketOptions.SslOnConnect,
_ => SecureSocketOptions.None
};
/// <returns>True if the connection is newly established.</returns>
public async Task<bool> EnsureConnectedAsync(IImapClient client)
{
if (client.IsConnected) return false;
client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback;
await client.ConnectAsync(_customServerInformation.IncomingServer,
int.Parse(_customServerInformation.IncomingServerPort),
GetSocketOptions(_customServerInformation.IncomingServerSocketOption));
// Print out useful information for testing.
if (client.IsConnected && ImapClientPoolOptions.IsTestPool)
{
// Print supported authentication methods for the client.
var supportedAuthMethods = client.AuthenticationMechanisms;
if (supportedAuthMethods == null || supportedAuthMethods.Count == 0)
{
WriteToProtocolLog("There are no supported authentication mechanisms...");
}
else
{
WriteToProtocolLog($"Supported authentication mechanisms: {string.Join(", ", supportedAuthMethods)}");
}
}
return true;
}
private void WriteToProtocolLog(string message)
{
if (_protocolLogStream == null) return;
try
{
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
_protocolLogStream.Write(messageBytes, 0, messageBytes.Length);
}
catch (ObjectDisposedException)
{
Log.Warning($"Protocol log stream is disposed. Cannot write to it.");
}
catch (Exception)
{
throw;
}
}
bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// If there are no errors, then everything went smoothly.
if (sslPolicyErrors == SslPolicyErrors.None) return true;
// Imap connectivity test will throw to alert the user here.
if (ThrowOnSSLHandshakeCallback)
{
throw new ImapTestSSLCertificateException(certificate.Issuer, certificate.GetExpirationDateString(), certificate.GetEffectiveDateString());
}
return true;
}
public async Task EnsureAuthenticatedAsync(IImapClient client)
{
if (client.IsAuthenticated) return;
var cred = new NetworkCredential(_customServerInformation.IncomingServerUsername, _customServerInformation.IncomingServerPassword);
var prefferedAuthenticationMethod = _customServerInformation.IncomingAuthenticationMethod;
if (prefferedAuthenticationMethod != ImapAuthenticationMethod.Auto)
{
// Anything beside Auto must be explicitly set for the client.
client.AuthenticationMechanisms.Clear();
var saslMechanism = GetSASLAuthenticationMethodName(prefferedAuthenticationMethod);
client.AuthenticationMechanisms.Add(saslMechanism);
var mechanism = SaslMechanism.Create(saslMechanism, cred);
await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred));
}
else
{
WriteToProtocolLog($"Supported authentication mechanisms: {string.Join(", ", supportedAuthMethods)}");
await client.AuthenticateAsync(cred);
}
}
return true;
}
private void WriteToProtocolLog(string message)
{
if (_protocolLogStream == null) return;
try
private string GetSASLAuthenticationMethodName(ImapAuthenticationMethod method)
{
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
_protocolLogStream.Write(messageBytes, 0, messageBytes.Length);
}
catch (ObjectDisposedException)
{
Log.Warning($"Protocol log stream is disposed. Cannot write to it.");
}
catch (Exception)
{
throw;
}
}
bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// If there are no errors, then everything went smoothly.
if (sslPolicyErrors == SslPolicyErrors.None) return true;
// Imap connectivity test will throw to alert the user here.
if (ThrowOnSSLHandshakeCallback)
{
throw new ImapTestSSLCertificateException(certificate.Issuer, certificate.GetExpirationDateString(), certificate.GetEffectiveDateString());
}
return true;
}
public async Task EnsureAuthenticatedAsync(IImapClient client)
{
if (client.IsAuthenticated) return;
var cred = new NetworkCredential(_customServerInformation.IncomingServerUsername, _customServerInformation.IncomingServerPassword);
var prefferedAuthenticationMethod = _customServerInformation.IncomingAuthenticationMethod;
if (prefferedAuthenticationMethod != ImapAuthenticationMethod.Auto)
{
// Anything beside Auto must be explicitly set for the client.
client.AuthenticationMechanisms.Clear();
var saslMechanism = GetSASLAuthenticationMethodName(prefferedAuthenticationMethod);
client.AuthenticationMechanisms.Add(saslMechanism);
var mechanism = SaslMechanism.Create(saslMechanism, cred);
await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred));
}
else
{
await client.AuthenticateAsync(cred);
}
}
private string GetSASLAuthenticationMethodName(ImapAuthenticationMethod method)
{
return method switch
{
ImapAuthenticationMethod.NormalPassword => "PLAIN",
ImapAuthenticationMethod.EncryptedPassword => "LOGIN",
ImapAuthenticationMethod.Ntlm => "NTLM",
ImapAuthenticationMethod.CramMd5 => "CRAM-MD5",
ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5",
_ => "PLAIN"
};
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
return method switch
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;
ImapAuthenticationMethod.NormalPassword => "PLAIN",
ImapAuthenticationMethod.EncryptedPassword => "LOGIN",
ImapAuthenticationMethod.Ntlm => "NTLM",
ImapAuthenticationMethod.CramMd5 => "CRAM-MD5",
ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5",
_ => "PLAIN"
};
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -6,55 +6,56 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Requests.Folder;
using Wino.Core.Requests.Mail;
namespace Wino.Core.Integration.Json;
public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver
namespace Wino.Core.Integration.Json
{
public ServerRequestTypeInfoResolver()
public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver
{
Modifiers.Add(new System.Action<JsonTypeInfo>(t =>
public ServerRequestTypeInfoResolver()
{
if (t.Type == typeof(IRequestBase))
Modifiers.Add(new System.Action<JsonTypeInfo>(t =>
{
t.PolymorphismOptions = new()
if (t.Type == typeof(IRequestBase))
{
DerivedTypes =
t.PolymorphismOptions = new()
{
new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)),
new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)),
new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)),
new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)),
new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)),
new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)),
new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)),
new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)),
new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)),
new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)),
new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)),
new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)),
}
};
}
else if (t.Type == typeof(IMailItem))
{
t.PolymorphismOptions = new JsonPolymorphismOptions()
DerivedTypes =
{
new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)),
new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)),
new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)),
new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)),
new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)),
new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)),
new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)),
new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)),
new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)),
new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)),
new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)),
new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)),
}
};
}
else if (t.Type == typeof(IMailItem))
{
DerivedTypes =
t.PolymorphismOptions = new JsonPolymorphismOptions()
{
new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)),
}
};
}
else if (t.Type == typeof(IMailFolder))
{
t.PolymorphismOptions = new JsonPolymorphismOptions()
DerivedTypes =
{
new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)),
}
};
}
else if (t.Type == typeof(IMailFolder))
{
DerivedTypes =
t.PolymorphismOptions = new JsonPolymorphismOptions()
{
new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)),
}
};
}
}));
DerivedTypes =
{
new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)),
}
};
}
}));
}
}
}

View File

@@ -11,201 +11,202 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Services;
namespace Wino.Core.Integration.Processors;
/// <summary>
/// Database change processor that handles common operations for all synchronizers.
/// When a synchronizer detects a change, it should call the appropriate method in this class to reflect the change in the database.
/// Different synchronizers might need additional implementations.
/// <see cref="IGmailChangeProcessor"/>, <see cref="IOutlookChangeProcessor"/> and <see cref="IImapChangeProcessor"/>
/// None of the synchronizers can directly change anything in the database.
/// </summary>
public interface IDefaultChangeProcessor
{
Task UpdateAccountAsync(MailAccount account);
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead);
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package);
Task DeleteMailAsync(Guid accountId, string mailId);
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
Task InsertFolderAsync(MailItemFolder folder);
Task UpdateFolderAsync(MailItemFolder folder);
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options);
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId);
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
// Calendar
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
Task DeleteCalendarItemAsync(Guid calendarItemId);
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
}
public interface IGmailChangeProcessor : IDefaultChangeProcessor
{
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
}
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
namespace Wino.Core.Integration.Processors
{
/// <summary>
/// Interrupted initial synchronization may cause downloaded mails to be saved in the database twice.
/// Since downloading mime is costly in Outlook, we need to check if the actual copy of the message has been saved before.
/// Database change processor that handles common operations for all synchronizers.
/// When a synchronizer detects a change, it should call the appropriate method in this class to reflect the change in the database.
/// Different synchronizers might need additional implementations.
/// <see cref="IGmailChangeProcessor"/>, <see cref="IOutlookChangeProcessor"/> and <see cref="IImapChangeProcessor"/>
/// None of the synchronizers can directly change anything in the database.
/// </summary>
/// <param name="messageId">MailCopyId of the message.</param>
/// <returns>Whether the mime has b</returns>
Task<bool> IsMailExistsAsync(string messageId);
public interface IDefaultChangeProcessor
{
Task UpdateAccountAsync(MailAccount account);
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead);
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package);
Task DeleteMailAsync(Guid accountId, string mailId);
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
Task InsertFolderAsync(MailItemFolder folder);
Task UpdateFolderAsync(MailItemFolder folder);
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options);
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId);
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
/// <summary>
/// Checks whether the mail exists in the folder.
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
/// </summary>
/// <param name="messageId">Message id</param>
/// <param name="folderId">Folder's local id.</param>
/// <returns>Whether mail exists in the folder or not.</returns>
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
// Calendar
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
/// <summary>
/// Updates Folder's delta synchronization identifier.
/// Only used in Outlook since it does per-folder sync.
/// </summary>
/// <param name="folderId">Folder id</param>
/// <param name="synchronizationIdentifier">New synchronization identifier.</param>
/// <returns>New identifier if success.</returns>
Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier);
Task DeleteCalendarItemAsync(Guid calendarItemId);
/// <summary>
/// Outlook may expire folder's delta token after a while.
/// Recommended action for this scenario is to reset token and do full sync.
/// This method resets the token for the given folder.
/// </summary>
/// <param name="folderId">Local folder id to reset token for.</param>
/// <returns>Empty string to assign folder delta sync for.</returns>
Task<string> ResetFolderDeltaTokenAsync(Guid folderId);
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
/// <summary>
/// Outlook may expire account's delta token after a while.
/// This will result returning 410 GONE response from the API for synchronizing folders.
/// This method resets the token for the given account for re-syncing folders.
/// </summary>
/// <param name="accountId">Account identifier to reset delta token for.</param>
/// <returns>Empty string to assign account delta sync for.</returns>
Task<string> ResetAccountDeltaTokenAsync(Guid accountId);
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
}
public interface IGmailChangeProcessor : IDefaultChangeProcessor
{
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
}
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
{
/// <summary>
/// Interrupted initial synchronization may cause downloaded mails to be saved in the database twice.
/// Since downloading mime is costly in Outlook, we need to check if the actual copy of the message has been saved before.
/// </summary>
/// <param name="messageId">MailCopyId of the message.</param>
/// <returns>Whether the mime has b</returns>
Task<bool> IsMailExistsAsync(string messageId);
/// <summary>
/// Checks whether the mail exists in the folder.
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
/// </summary>
/// <param name="messageId">Message id</param>
/// <param name="folderId">Folder's local id.</param>
/// <returns>Whether mail exists in the folder or not.</returns>
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
/// <summary>
/// Updates Folder's delta synchronization identifier.
/// Only used in Outlook since it does per-folder sync.
/// </summary>
/// <param name="folderId">Folder id</param>
/// <param name="synchronizationIdentifier">New synchronization identifier.</param>
/// <returns>New identifier if success.</returns>
Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier);
/// <summary>
/// Outlook may expire folder's delta token after a while.
/// Recommended action for this scenario is to reset token and do full sync.
/// This method resets the token for the given folder.
/// </summary>
/// <param name="folderId">Local folder id to reset token for.</param>
/// <returns>Empty string to assign folder delta sync for.</returns>
Task<string> ResetFolderDeltaTokenAsync(Guid folderId);
/// <summary>
/// Outlook may expire account's delta token after a while.
/// This will result returning 410 GONE response from the API for synchronizing folders.
/// This method resets the token for the given account for re-syncing folders.
/// </summary>
/// <param name="accountId">Account identifier to reset delta token for.</param>
/// <returns>Empty string to assign account delta sync for.</returns>
Task<string> ResetAccountDeltaTokenAsync(Guid accountId);
Task ManageCalendarEventAsync(Microsoft.Graph.Models.Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
Task ManageCalendarEventAsync(Microsoft.Graph.Models.Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
}
public interface IImapChangeProcessor : IDefaultChangeProcessor
{
/// <summary>
/// Returns all known uids for the given folder.
/// </summary>
/// <param name="folderId">Folder id to retrieve uIds for.</param>
Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId);
}
public class DefaultChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
{
protected IMailService MailService = mailService;
protected ICalendarService CalendarService = calendarService;
protected IFolderService FolderService = folderService;
protected IAccountService AccountService = accountService;
private readonly IMimeFileService _mimeFileService = mimeFileService;
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier);
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
=> MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged);
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
=> MailService.ChangeReadStatusAsync(mailCopyId, isRead);
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public Task DeleteMailAsync(Guid accountId, string mailId)
=> MailService.DeleteMailAsync(accountId, mailId);
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
=> MailService.CreateMailAsync(accountId, package);
public Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId)
=> FolderService.GetFoldersAsync(accountId);
public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
public Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId)
=> FolderService.GetFoldersAsync(accountId);
public Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options)
=> FolderService.GetSynchronizationFoldersAsync(options);
public Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
=> FolderService.DeleteFolderAsync(accountId, remoteFolderId);
public Task InsertFolderAsync(MailItemFolder folder)
=> FolderService.InsertFolderAsync(folder);
public Task UpdateFolderAsync(MailItemFolder folder)
=> FolderService.UpdateFolderAsync(folder);
public Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds)
=> MailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds);
public Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
=> _mimeFileService.SaveMimeMessageAsync(fileId, mimeMessage, accountId);
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(folderId);
public Task UpdateAccountAsync(MailAccount account)
=> AccountService.UpdateAccountAsync(account);
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
=> CalendarService.GetAccountCalendarsAsync(accountId);
public Task DeleteCalendarItemAsync(Guid calendarItemId)
=> CalendarService.DeleteCalendarItemAsync(calendarItemId);
public Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.DeleteAccountCalendarAsync(accountCalendar);
public Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.InsertAccountCalendarAsync(accountCalendar);
public Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.UpdateAccountCalendarAsync(accountCalendar);
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
=> CalendarService.UpdateCalendarDeltaSynchronizationToken(calendarId, deltaToken);
}
public interface IImapChangeProcessor : IDefaultChangeProcessor
{
/// <summary>
/// Returns all known uids for the given folder.
/// </summary>
/// <param name="folderId">Folder id to retrieve uIds for.</param>
Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId);
}
public class DefaultChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
{
protected IMailService MailService = mailService;
protected ICalendarService CalendarService = calendarService;
protected IFolderService FolderService = folderService;
protected IAccountService AccountService = accountService;
private readonly IMimeFileService _mimeFileService = mimeFileService;
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier);
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
=> MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged);
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
=> MailService.ChangeReadStatusAsync(mailCopyId, isRead);
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public Task DeleteMailAsync(Guid accountId, string mailId)
=> MailService.DeleteMailAsync(accountId, mailId);
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
=> MailService.CreateMailAsync(accountId, package);
public Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId)
=> FolderService.GetFoldersAsync(accountId);
public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
public Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId)
=> FolderService.GetFoldersAsync(accountId);
public Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options)
=> FolderService.GetSynchronizationFoldersAsync(options);
public Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
=> FolderService.DeleteFolderAsync(accountId, remoteFolderId);
public Task InsertFolderAsync(MailItemFolder folder)
=> FolderService.InsertFolderAsync(folder);
public Task UpdateFolderAsync(MailItemFolder folder)
=> FolderService.UpdateFolderAsync(folder);
public Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds)
=> MailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds);
public Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
=> _mimeFileService.SaveMimeMessageAsync(fileId, mimeMessage, accountId);
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(folderId);
public Task UpdateAccountAsync(MailAccount account)
=> AccountService.UpdateAccountAsync(account);
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
=> CalendarService.GetAccountCalendarsAsync(accountId);
public Task DeleteCalendarItemAsync(Guid calendarItemId)
=> CalendarService.DeleteCalendarItemAsync(calendarItemId);
public Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.DeleteAccountCalendarAsync(accountCalendar);
public Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.InsertAccountCalendarAsync(accountCalendar);
public Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
=> CalendarService.UpdateAccountCalendarAsync(accountCalendar);
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
=> CalendarService.UpdateCalendarDeltaSynchronizationToken(calendarId, deltaToken);
}
}

View File

@@ -13,299 +13,300 @@ using Wino.Services;
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
using CalendarItem = Wino.Core.Domain.Entities.Calendar.CalendarItem;
namespace Wino.Core.Integration.Processors;
public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcessor
namespace Wino.Core.Integration.Processors
{
public GmailChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcessor
{
}
public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId);
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
var status = calendarEvent.Status;
var recurringEventId = calendarEvent.RecurringEventId;
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
// Check if we have this event before.
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
if (existingCalendarItem == null)
public GmailChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
{
CalendarItem parentRecurringEvent = null;
// Manage the recurring event id.
if (!string.IsNullOrEmpty(recurringEventId))
{
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
if (parentRecurringEvent == null)
{
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// We don't have this event yet. Create a new one.
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
double totalDurationInSeconds = 0;
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
{
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
}
CalendarItem calendarItem = null;
if (parentRecurringEvent != null)
{
// Exceptions of parent events might not have all the fields populated.
// We must use the parent event's data for fields that don't exists.
// Update duration if it's not populated.
if (totalDurationInSeconds == 0)
{
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
}
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
// Leave it empty if it's not populated.
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
};
}
else
{
// This is a parent event creation.
// Start-End dates are guaranteed to be populated.
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
{
Log.Error("Failed to create parent event because either start or end date is not specified.");
return;
}
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
};
}
// Hide canceled events.
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
// Manage the recurring event id.
if (parentRecurringEvent != null)
{
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
}
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
// Attendees
var attendees = new List<CalendarEventAttendee>();
if (calendarEvent.Attendees == null)
{
// Self-only event.
attendees.Add(new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = true,
Email = organizerAccount.Address,
Name = organizerAccount.SenderName,
AttendenceStatus = AttendeeStatus.Accepted,
Id = Guid.NewGuid(),
IsOptionalAttendee = false,
});
}
else
{
foreach (var attendee in calendarEvent.Attendees)
{
if (attendee.Self == true)
{
// TODO:
}
else if (!string.IsNullOrEmpty(attendee.Email))
{
AttendeeStatus GetAttendenceStatus(string responseStatus)
{
return responseStatus switch
{
"accepted" => AttendeeStatus.Accepted,
"declined" => AttendeeStatus.Declined,
"tentative" => AttendeeStatus.Tentative,
"needsAction" => AttendeeStatus.NeedsAction,
_ => AttendeeStatus.NeedsAction
};
}
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = attendee.Organizer ?? false,
Comment = attendee.Comment,
Email = attendee.Email,
Name = attendee.DisplayName,
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
Id = Guid.NewGuid(),
IsOptionalAttendee = attendee.Optional ?? false,
};
attendees.Add(eventAttendee);
}
}
}
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
}
else
public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId);
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// We have this event already. Update it.
if (calendarEvent.Status == "cancelled")
var status = calendarEvent.Status;
var recurringEventId = calendarEvent.RecurringEventId;
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
// Check if we have this event before.
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
if (existingCalendarItem == null)
{
// Parent event is canceled. We must delete everything.
if (string.IsNullOrEmpty(recurringEventId))
CalendarItem parentRecurringEvent = null;
// Manage the recurring event id.
if (!string.IsNullOrEmpty(recurringEventId))
{
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
if (parentRecurringEvent == null)
{
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
return;
// We don't have this event yet. Create a new one.
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
double totalDurationInSeconds = 0;
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
{
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
}
CalendarItem calendarItem = null;
if (parentRecurringEvent != null)
{
// Exceptions of parent events might not have all the fields populated.
// We must use the parent event's data for fields that don't exists.
// Update duration if it's not populated.
if (totalDurationInSeconds == 0)
{
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
}
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
// Leave it empty if it's not populated.
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
};
}
else
{
// Child event is canceled.
// Child should live as long as parent lives, but must not be displayed to the user.
// This is a parent event creation.
// Start-End dates are guaranteed to be populated.
existingCalendarItem.IsHidden = true;
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
{
Log.Error("Failed to create parent event because either start or end date is not specified.");
return;
}
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
};
}
// Hide canceled events.
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
// Manage the recurring event id.
if (parentRecurringEvent != null)
{
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
}
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
// Attendees
var attendees = new List<CalendarEventAttendee>();
if (calendarEvent.Attendees == null)
{
// Self-only event.
attendees.Add(new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = true,
Email = organizerAccount.Address,
Name = organizerAccount.SenderName,
AttendenceStatus = AttendeeStatus.Accepted,
Id = Guid.NewGuid(),
IsOptionalAttendee = false,
});
}
else
{
foreach (var attendee in calendarEvent.Attendees)
{
if (attendee.Self == true)
{
// TODO:
}
else if (!string.IsNullOrEmpty(attendee.Email))
{
AttendeeStatus GetAttendenceStatus(string responseStatus)
{
return responseStatus switch
{
"accepted" => AttendeeStatus.Accepted,
"declined" => AttendeeStatus.Declined,
"tentative" => AttendeeStatus.Tentative,
"needsAction" => AttendeeStatus.NeedsAction,
_ => AttendeeStatus.NeedsAction
};
}
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = attendee.Organizer ?? false,
Comment = attendee.Comment,
Email = attendee.Email,
Name = attendee.DisplayName,
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
Id = Guid.NewGuid(),
IsOptionalAttendee = attendee.Optional ?? false,
};
attendees.Add(eventAttendee);
}
}
}
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
}
else
{
// Make sure to unhide the event.
// It might be marked as hidden before.
existingCalendarItem.IsHidden = false;
// We have this event already. Update it.
if (calendarEvent.Status == "cancelled")
{
// Parent event is canceled. We must delete everything.
if (string.IsNullOrEmpty(recurringEventId))
{
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
// Update the event properties.
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
return;
}
else
{
// Child event is canceled.
// Child should live as long as parent lives, but must not be displayed to the user.
existingCalendarItem.IsHidden = true;
}
}
else
{
// Make sure to unhide the event.
// It might be marked as hidden before.
existingCalendarItem.IsHidden = false;
// Update the event properties.
}
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(existingCalendarItem);
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(existingCalendarItem);
}
private string GetOrganizerName(Event calendarEvent, MailAccount account)
{
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
private string GetOrganizerName(Event calendarEvent, MailAccount account)
{
return account.SenderName;
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
{
return account.SenderName;
}
else
return calendarEvent.Organizer.DisplayName;
}
else
return calendarEvent.Organizer.DisplayName;
}
private string GetOrganizerEmail(Event calendarEvent, MailAccount account)
{
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
private string GetOrganizerEmail(Event calendarEvent, MailAccount account)
{
return account.Address;
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
{
return account.Address;
}
else
return calendarEvent.Organizer.Email;
}
else
return calendarEvent.Organizer.Email;
}
private CalendarItemStatus GetStatus(string status)
{
return status switch
private CalendarItemStatus GetStatus(string status)
{
"confirmed" => CalendarItemStatus.Confirmed,
"tentative" => CalendarItemStatus.Tentative,
"cancelled" => CalendarItemStatus.Cancelled,
_ => CalendarItemStatus.Confirmed
};
}
return status switch
{
"confirmed" => CalendarItemStatus.Confirmed,
"tentative" => CalendarItemStatus.Tentative,
"cancelled" => CalendarItemStatus.Cancelled,
_ => CalendarItemStatus.Confirmed
};
}
private CalendarItemVisibility GetVisibility(string visibility)
{
/// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for
/// events on the calendar. This is the default value. - "public" - The event is public and event details are
/// visible to all readers of the calendar. - "private" - The event is private and only event attendees may
/// view event details. - "confidential" - The event is private. This value is provided for compatibility
/// reasons.
return visibility switch
private CalendarItemVisibility GetVisibility(string visibility)
{
"default" => CalendarItemVisibility.Default,
"public" => CalendarItemVisibility.Public,
"private" => CalendarItemVisibility.Private,
"confidential" => CalendarItemVisibility.Confidential,
_ => CalendarItemVisibility.Default
};
}
/// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for
/// events on the calendar. This is the default value. - "public" - The event is public and event details are
/// visible to all readers of the calendar. - "private" - The event is private and only event attendees may
/// view event details. - "confidential" - The event is private. This value is provided for compatibility
/// reasons.
return visibility switch
{
"default" => CalendarItemVisibility.Default,
"public" => CalendarItemVisibility.Public,
"private" => CalendarItemVisibility.Private,
"confidential" => CalendarItemVisibility.Confidential,
_ => CalendarItemVisibility.Default
};
}
}
}

View File

@@ -4,18 +4,19 @@ using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Services;
namespace Wino.Core.Integration.Processors;
public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
namespace Wino.Core.Integration.Processors
{
public ImapChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
IAccountService accountService,
ICalendarService calendarService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
{
}
public ImapChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
IMailService mailService,
IAccountService accountService,
ICalendarService calendarService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
{
}
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId) => FolderService.GetKnownUidsForFolderAsync(folderId);
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId) => FolderService.GetKnownUidsForFolderAsync(folderId);
}
}

View File

@@ -10,141 +10,142 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Extensions;
using Wino.Services;
namespace Wino.Core.Integration.Processors;
public class OutlookChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
ICalendarService calendarService,
IMailService mailService,
IAccountService accountService,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
, IOutlookChangeProcessor
namespace Wino.Core.Integration.Processors
{
public Task<bool> IsMailExistsAsync(string messageId)
=> MailService.IsMailExistsAsync(messageId);
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);
public async Task<string> ResetFolderDeltaTokenAsync(Guid folderId)
public class OutlookChangeProcessor(IDatabaseService databaseService,
IFolderService folderService,
ICalendarService calendarService,
IMailService mailService,
IAccountService accountService,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
, IOutlookChangeProcessor
{
var folder = await FolderService.GetFolderAsync(folderId);
public Task<bool> IsMailExistsAsync(string messageId)
=> MailService.IsMailExistsAsync(messageId);
folder.DeltaToken = null;
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
await FolderService.UpdateFolderAsync(folder);
public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);
return string.Empty;
}
public Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier)
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?", synchronizationIdentifier, folderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// We parse the occurrences based on the parent event.
// There is literally no point to store them because
// type=Exception events are the exceptional childs of recurrency parent event.
if (calendarEvent.Type == EventType.Occurrence) return;
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
Guid savingItemId = Guid.Empty;
if (savingItem != null)
savingItemId = savingItem.Id;
else
public async Task<string> ResetFolderDeltaTokenAsync(Guid folderId)
{
savingItemId = Guid.NewGuid();
savingItem = new CalendarItem() { Id = savingItemId };
var folder = await FolderService.GetFolderAsync(folderId);
folder.DeltaToken = null;
await FolderService.UpdateFolderAsync(folder);
return string.Empty;
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
public Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier)
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?", synchronizationIdentifier, folderId);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
savingItem.RemoteEventId = calendarEvent.Id;
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// This is a recurring event exception.
// We need to find the parent event and set it as recurring event id.
// We parse the occurrences based on the parent event.
// There is literally no point to store them because
// type=Exception events are the exceptional childs of recurrency parent event.
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
if (calendarEvent.Type == EventType.Occurrence) return;
if (parentEvent != null)
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
Guid savingItemId = Guid.Empty;
if (savingItem != null)
savingItemId = savingItem.Id;
else
{
savingItem.RecurringCalendarItemId = parentEvent.Id;
savingItemId = Guid.NewGuid();
savingItem = new CalendarItem() { Id = savingItemId };
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
savingItem.RemoteEventId = calendarEvent.Id;
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
{
// This is a recurring event exception.
// We need to find the parent event and set it as recurring event id.
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
if (parentEvent != null)
{
savingItem.RecurringCalendarItemId = parentEvent.Id;
}
else
{
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// Convert the recurrence pattern to string for parent recurring events.
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
{
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
}
savingItem.HtmlLink = calendarEvent.WebLink;
savingItem.CalendarId = assignedCalendar.Id;
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
{
case ResponseType.None:
case ResponseType.NotResponded:
savingItem.Status = CalendarItemStatus.NotResponded;
break;
case ResponseType.TentativelyAccepted:
savingItem.Status = CalendarItemStatus.Tentative;
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
savingItem.IsHidden = true;
break;
default:
break;
}
}
else
{
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
savingItem.Status = CalendarItemStatus.Confirmed;
}
}
// Convert the recurrence pattern to string for parent recurring events.
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
{
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(savingItem);
savingItem.HtmlLink = calendarEvent.WebLink;
savingItem.CalendarId = assignedCalendar.Id;
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
// Manage attendees.
if (calendarEvent.Attendees != null)
{
case ResponseType.None:
case ResponseType.NotResponded:
savingItem.Status = CalendarItemStatus.NotResponded;
break;
case ResponseType.TentativelyAccepted:
savingItem.Status = CalendarItemStatus.Tentative;
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
savingItem.IsHidden = true;
break;
default:
break;
// Clear all attendees for this event.
var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList();
await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false);
}
}
else
{
savingItem.Status = CalendarItemStatus.Confirmed;
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(savingItem);
// Manage attendees.
if (calendarEvent.Attendees != null)
{
// Clear all attendees for this event.
var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList();
await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false);
}
}
}

View File

@@ -2,58 +2,59 @@
using MailKit.Net.Imap;
using Serilog;
namespace Wino.Core.Integration;
/// <summary>
/// Extended class for ImapClient that is used in Wino.
/// </summary>
internal class WinoImapClient : ImapClient
namespace Wino.Core.Integration
{
/// <summary>
/// Gets or internally sets whether the QRESYNC extension is enabled.
/// It is set by ImapClientPool immidiately after the authentication.
/// Extended class for ImapClient that is used in Wino.
/// </summary>
public bool IsQResyncEnabled { get; internal set; }
public WinoImapClient()
internal class WinoImapClient : ImapClient
{
HookEvents();
}
/// <summary>
/// Gets or internally sets whether the QRESYNC extension is enabled.
/// It is set by ImapClientPool immidiately after the authentication.
/// </summary>
public bool IsQResyncEnabled { get; internal set; }
public WinoImapClient(IProtocolLogger protocolLogger) : base(protocolLogger)
{
HookEvents();
}
private void HookEvents()
{
Disconnected += ClientDisconnected;
}
private void UnhookEvents()
{
Disconnected -= ClientDisconnected;
}
private void ClientDisconnected(object sender, DisconnectedEventArgs e)
{
if (e.IsRequested)
public WinoImapClient()
{
Log.Debug("Imap client is disconnected on request.");
HookEvents();
}
else
public WinoImapClient(IProtocolLogger protocolLogger) : base(protocolLogger)
{
Log.Debug("Imap client connection is dropped by server.");
HookEvents();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
private void HookEvents()
{
UnhookEvents();
Disconnected += ClientDisconnected;
}
private void UnhookEvents()
{
Disconnected -= ClientDisconnected;
}
private void ClientDisconnected(object sender, DisconnectedEventArgs e)
{
if (e.IsRequested)
{
Log.Debug("Imap client is disconnected on request.");
}
else
{
Log.Debug("Imap client connection is dropped by server.");
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
UnhookEvents();
}
}
}
}

View File

@@ -1,49 +1,50 @@
using System;
using System.Drawing;
namespace Wino.Core.Misc;
public static class ColorHelpers
namespace Wino.Core.Misc
{
public static string GenerateFlatColorHex()
public static class ColorHelpers
{
Random random = new();
int hue = random.Next(0, 360); // Full hue range
int saturation = 70 + random.Next(30); // High saturation (70-100%)
int lightness = 50 + random.Next(20); // Bright colors (50-70%)
public static string GenerateFlatColorHex()
{
Random random = new();
int hue = random.Next(0, 360); // Full hue range
int saturation = 70 + random.Next(30); // High saturation (70-100%)
int lightness = 50 + random.Next(20); // Bright colors (50-70%)
var color = FromHsl(hue, saturation, lightness);
var color = FromHsl(hue, saturation, lightness);
return ToHexString(color);
}
return ToHexString(color);
}
public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})";
public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})";
private static Color FromHsl(int h, int s, int l)
{
double hue = h / 360.0;
double saturation = s / 100.0;
double lightness = l / 100.0;
private static Color FromHsl(int h, int s, int l)
{
double hue = h / 360.0;
double saturation = s / 100.0;
double lightness = l / 100.0;
// Conversion from HSL to RGB
var chroma = (1 - Math.Abs(2 * lightness - 1)) * saturation;
var x = chroma * (1 - Math.Abs((hue * 6) % 2 - 1));
var m = lightness - chroma / 2;
// Conversion from HSL to RGB
var chroma = (1 - Math.Abs(2 * lightness - 1)) * saturation;
var x = chroma * (1 - Math.Abs((hue * 6) % 2 - 1));
var m = lightness - chroma / 2;
double r = 0, g = 0, b = 0;
double r = 0, g = 0, b = 0;
if (hue < 1.0 / 6.0) { r = chroma; g = x; b = 0; }
else if (hue < 2.0 / 6.0) { r = x; g = chroma; b = 0; }
else if (hue < 3.0 / 6.0) { r = 0; g = chroma; b = x; }
else if (hue < 4.0 / 6.0) { r = 0; g = x; b = chroma; }
else if (hue < 5.0 / 6.0) { r = x; g = 0; b = chroma; }
else { r = chroma; g = 0; b = x; }
if (hue < 1.0 / 6.0) { r = chroma; g = x; b = 0; }
else if (hue < 2.0 / 6.0) { r = x; g = chroma; b = 0; }
else if (hue < 3.0 / 6.0) { r = 0; g = chroma; b = x; }
else if (hue < 4.0 / 6.0) { r = 0; g = x; b = chroma; }
else if (hue < 5.0 / 6.0) { r = x; g = 0; b = chroma; }
else { r = chroma; g = 0; b = x; }
return Color.FromArgb(
(int)((r + m) * 255),
(int)((g + m) * 255),
(int)((b + m) * 255));
return Color.FromArgb(
(int)((r + m) * 255),
(int)((g + m) * 255),
(int)((b + m) * 255));
}
}
}

View File

@@ -1,24 +1,25 @@
using System.Text.Json.Serialization;
namespace Wino.Core.Misc;
public class OutlookFileAttachment
namespace Wino.Core.Misc
{
[JsonPropertyName("@odata.type")]
public string OdataType { get; } = "#microsoft.graph.fileAttachment";
public class OutlookFileAttachment
{
[JsonPropertyName("@odata.type")]
public string OdataType { get; } = "#microsoft.graph.fileAttachment";
[JsonPropertyName("name")]
public string FileName { get; set; }
[JsonPropertyName("name")]
public string FileName { get; set; }
[JsonPropertyName("contentBytes")]
public string Base64EncodedContentBytes { get; set; }
[JsonPropertyName("contentBytes")]
public string Base64EncodedContentBytes { get; set; }
[JsonPropertyName("contentType")]
public string ContentType { get; set; }
[JsonPropertyName("contentType")]
public string ContentType { get; set; }
[JsonPropertyName("contentId")]
public string ContentId { get; set; }
[JsonPropertyName("contentId")]
public string ContentId { get; set; }
[JsonPropertyName("isInline")]
public bool IsInline { get; set; }
[JsonPropertyName("isInline")]
public bool IsInline { get; set; }
}
}

View File

@@ -5,25 +5,26 @@ using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Requests.Bundles;
public record HttpRequestBundle<TRequest>(TRequest NativeRequest, IUIChangeRequest UIChangeRequest, IRequestBase Request = null) : IRequestBundle<TRequest>
namespace Wino.Core.Requests.Bundles
{
public string BundleId { get; set; } = string.Empty;
}
/// <summary>
/// Bundle that encapsulates batch request and native request with response.
/// </summary>
/// <typeparam name="TRequest">Http type for each integrator. eg. ClientServiceRequest for Gmail and RequestInformation for Microsoft Graph.</typeparam>
/// <param name="NativeRequest">Native type to send via http.</param>
/// <param name="BatchRequest">Batch request that is generated by base synchronizer.</param>
public record HttpRequestBundle<TRequest, TResponse>(TRequest NativeRequest, IRequestBase Request) : HttpRequestBundle<TRequest>(NativeRequest, Request)
{
public async Task<TResponse> DeserializeBundleAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default)
public record HttpRequestBundle<TRequest>(TRequest NativeRequest, IUIChangeRequest UIChangeRequest, IRequestBase Request = null) : IRequestBundle<TRequest>
{
var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
public string BundleId { get; set; } = string.Empty;
}
return JsonSerializer.Deserialize<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
/// <summary>
/// Bundle that encapsulates batch request and native request with response.
/// </summary>
/// <typeparam name="TRequest">Http type for each integrator. eg. ClientServiceRequest for Gmail and RequestInformation for Microsoft Graph.</typeparam>
/// <param name="NativeRequest">Native type to send via http.</param>
/// <param name="BatchRequest">Batch request that is generated by base synchronizer.</param>
public record HttpRequestBundle<TRequest, TResponse>(TRequest NativeRequest, IRequestBase Request) : HttpRequestBundle<TRequest>(NativeRequest, Request)
{
public async Task<TResponse> DeserializeBundleAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default)
{
var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
}
}
}

View File

@@ -1,22 +1,23 @@
using System;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Requests.Bundles;
///// <summary>
///// Encapsulates request to queue and account for synchronizer.
///// </summary>
///// <param name="AccountId"><inheritdoc/></param>
///// <param name="Request"></param>
///// <param name="Request">Prepared request for the server.</param>
///// <param name="AccountId">Which account to execute this request for.</param>
public class ServerRequestBundle(Guid accountId, IRequestBase request) : IClientMessage
namespace Wino.Core.Requests.Bundles
{
public Guid AccountId { get; } = accountId;
///// <summary>
///// Encapsulates request to queue and account for synchronizer.
///// </summary>
///// <param name="AccountId"><inheritdoc/></param>
///// <param name="Request"></param>
///// <param name="Request">Prepared request for the server.</param>
///// <param name="AccountId">Which account to execute this request for.</param>
public class ServerRequestBundle(Guid accountId, IRequestBase request) : IClientMessage
{
public Guid AccountId { get; } = accountId;
public IRequestBase Request { get; } = request;
public IRequestBase Request { get; } = request;
}
//public record ServerRequestPackage<TUserActionRequestType>(Guid AccountId, TUserActionRequestType Request)
// : ServerRequestBundle(AccountId), IClientMessage where TUserActionRequestType : IRequestBase;
}
//public record ServerRequestPackage<TUserActionRequestType>(Guid AccountId, TUserActionRequestType Request)
// : ServerRequestBundle(AccountId), IClientMessage where TUserActionRequestType : IRequestBase;

View File

@@ -3,29 +3,30 @@ using System.Threading.Tasks;
using MailKit.Net.Imap;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Requests.Bundles;
public class ImapRequest
namespace Wino.Core.Requests.Bundles
{
public Func<IImapClient, IRequestBase, Task> IntegratorTask { get; }
public IRequestBase Request { get; }
public ImapRequest(Func<IImapClient, IRequestBase, Task> integratorTask, IRequestBase request)
public class ImapRequest
{
IntegratorTask = integratorTask;
Request = request;
public Func<IImapClient, IRequestBase, Task> IntegratorTask { get; }
public IRequestBase Request { get; }
public ImapRequest(Func<IImapClient, IRequestBase, Task> integratorTask, IRequestBase request)
{
IntegratorTask = integratorTask;
Request = request;
}
}
public class ImapRequest<TRequestBaseType> : ImapRequest where TRequestBaseType : IRequestBase
{
public ImapRequest(Func<IImapClient, TRequestBaseType, Task> integratorTask, TRequestBaseType request)
: base((client, request) => integratorTask(client, (TRequestBaseType)request), request)
{
}
}
public record ImapRequestBundle(ImapRequest NativeRequest, IRequestBase Request, IUIChangeRequest UIChangeRequest) : IRequestBundle<ImapRequest>
{
public string BundleId { get; set; } = Guid.NewGuid().ToString();
}
}
public class ImapRequest<TRequestBaseType> : ImapRequest where TRequestBaseType : IRequestBase
{
public ImapRequest(Func<IImapClient, TRequestBaseType, Task> integratorTask, TRequestBaseType request)
: base((client, request) => integratorTask(client, (TRequestBaseType)request), request)
{
}
}
public record ImapRequestBundle(ImapRequest NativeRequest, IRequestBase Request, IUIChangeRequest UIChangeRequest) : IRequestBundle<ImapRequest>
{
public string BundleId { get; set; } = Guid.NewGuid().ToString();
}

View File

@@ -7,26 +7,27 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Folder;
public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDelete) : FolderRequestBase(Folder, FolderSynchronizerOperation.EmptyFolder), ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Folder
{
public bool ExcludeMustHaveFolders => false;
public override void ApplyUIChanges()
public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDelete) : FolderRequestBase(Folder, FolderSynchronizerOperation.EmptyFolder), ICustomFolderSynchronizationRequest
{
foreach (var item in MailsToDelete)
public bool ExcludeMustHaveFolders => false;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(item));
foreach (var item in MailsToDelete)
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(item));
}
}
}
public override void RevertUIChanges()
{
foreach (var item in MailsToDelete)
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(item));
foreach (var item in MailsToDelete)
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(item));
}
}
}
public List<Guid> SynchronizationFolderIds => [Folder.Id];
public List<Guid> SynchronizationFolderIds => [Folder.Id];
}
}

View File

@@ -7,31 +7,32 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Folder;
public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> MailsToMarkRead) : FolderRequestBase(Folder, FolderSynchronizerOperation.MarkFolderRead), ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Folder
{
public override void ApplyUIChanges()
public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> MailsToMarkRead) : FolderRequestBase(Folder, FolderSynchronizerOperation.MarkFolderRead), ICustomFolderSynchronizationRequest
{
foreach (var item in MailsToMarkRead)
public override void ApplyUIChanges()
{
item.IsRead = true;
foreach (var item in MailsToMarkRead)
{
item.IsRead = true;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
}
}
}
public override void RevertUIChanges()
{
foreach (var item in MailsToMarkRead)
public override void RevertUIChanges()
{
item.IsRead = false;
foreach (var item in MailsToMarkRead)
{
item.IsRead = false;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
}
}
public List<Guid> SynchronizationFolderIds => [Folder.Id];
public bool ExcludeMustHaveFolders => true;
}
public List<Guid> SynchronizationFolderIds => [Folder.Id];
public bool ExcludeMustHaveFolders => true;
}

View File

@@ -4,19 +4,20 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Folder;
public record RenameFolderRequest(MailItemFolder Folder, string CurrentFolderName, string NewFolderName) : FolderRequestBase(Folder, FolderSynchronizerOperation.RenameFolder)
namespace Wino.Core.Requests.Folder
{
public override void ApplyUIChanges()
public record RenameFolderRequest(MailItemFolder Folder, string CurrentFolderName, string NewFolderName) : FolderRequestBase(Folder, FolderSynchronizerOperation.RenameFolder)
{
Folder.FolderName = NewFolderName;
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
}
public override void ApplyUIChanges()
{
Folder.FolderName = NewFolderName;
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
}
public override void RevertUIChanges()
{
Folder.FolderName = CurrentFolderName;
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
public override void RevertUIChanges()
{
Folder.FolderName = CurrentFolderName;
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
}
}
}

View File

@@ -3,16 +3,17 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Requests;
namespace Wino.Core.Requests.Mail;
public record AlwaysMoveToRequest(MailCopy Item, bool MoveToFocused) : MailRequestBase(Item)
namespace Wino.Core.Requests.Mail
{
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.AlwaysMoveTo;
}
public class BatchAlwaysMoveToRequest : BatchCollection<AlwaysMoveToRequest>
{
public BatchAlwaysMoveToRequest(IEnumerable<AlwaysMoveToRequest> collection) : base(collection)
public record AlwaysMoveToRequest(MailCopy Item, bool MoveToFocused) : MailRequestBase(Item)
{
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.AlwaysMoveTo;
}
public class BatchAlwaysMoveToRequest : BatchCollection<AlwaysMoveToRequest>
{
public BatchAlwaysMoveToRequest(IEnumerable<AlwaysMoveToRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,52 +7,53 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
/// <summary>
/// Archive message request.
/// By default, the message will be moved to the Archive folder.
/// For Gmail, 'Archive' label will be removed from the message.
/// </summary>
/// <param name="IsArchiving">Whether are archiving or unarchiving</param>
/// <param name="Item">Mail to archive</param>
/// <param name="FromFolder">Source folder.</param>
/// <param name="ToFolder">Optional Target folder. Required for ImapSynchronizer and OutlookSynchronizer.</param>
public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder = null)
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public bool ExcludeMustHaveFolders => false;
public List<Guid> SynchronizationFolderIds
/// <summary>
/// Archive message request.
/// By default, the message will be moved to the Archive folder.
/// For Gmail, 'Archive' label will be removed from the message.
/// </summary>
/// <param name="IsArchiving">Whether are archiving or unarchiving</param>
/// <param name="Item">Mail to archive</param>
/// <param name="FromFolder">Source folder.</param>
/// <param name="ToFolder">Optional Target folder. Required for ImapSynchronizer and OutlookSynchronizer.</param>
public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder = null)
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
{
get
public bool ExcludeMustHaveFolders => false;
public List<Guid> SynchronizationFolderIds
{
var folderIds = new List<Guid> { FromFolder.Id };
if (ToFolder != null)
get
{
folderIds.Add(ToFolder.Id);
}
var folderIds = new List<Guid> { FromFolder.Id };
return folderIds;
if (ToFolder != null)
{
folderIds.Add(ToFolder.Id);
}
return folderIds;
}
}
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Archive;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Archive;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public class BatchArchiveRequest : BatchCollection<ArchiveRequest>
{
public BatchArchiveRequest(IEnumerable<ArchiveRequest> collection) : base(collection)
public class BatchArchiveRequest : BatchCollection<ArchiveRequest>
{
public BatchArchiveRequest(IEnumerable<ArchiveRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,35 +7,36 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase(Item),
ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
public bool ExcludeMustHaveFolders => true;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
public override void ApplyUIChanges()
public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase(Item),
ICustomFolderSynchronizationRequest
{
Item.IsFlagged = IsFlagged;
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
public bool ExcludeMustHaveFolders => true;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
public override void ApplyUIChanges()
{
Item.IsFlagged = IsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
public override void RevertUIChanges()
{
Item.IsFlagged = !IsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
}
public override void RevertUIChanges()
{
Item.IsFlagged = !IsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
}
public class BatchChangeFlagRequest : BatchCollection<ChangeFlagRequest>
{
public BatchChangeFlagRequest(IEnumerable<ChangeFlagRequest> collection) : base(collection)
public class BatchChangeFlagRequest : BatchCollection<ChangeFlagRequest>
{
public BatchChangeFlagRequest(IEnumerable<ChangeFlagRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,23 +7,24 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest)
: MailRequestBase(DraftPreperationRequest.CreatedLocalDraftCopy),
ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public bool ExcludeMustHaveFolders => false;
public List<Guid> SynchronizationFolderIds =>
[
DraftPreperationRequest.CreatedLocalDraftCopy.AssignedFolder.Id
];
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.CreateDraft;
public override void RevertUIChanges()
public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest)
: MailRequestBase(DraftPreperationRequest.CreatedLocalDraftCopy),
ICustomFolderSynchronizationRequest
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
public bool ExcludeMustHaveFolders => false;
public List<Guid> SynchronizationFolderIds =>
[
DraftPreperationRequest.CreatedLocalDraftCopy.AssignedFolder.Id
];
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.CreateDraft;
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
}
}

View File

@@ -7,33 +7,34 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
/// <summary>
/// Hard delete request. This request will delete the mail item from the server without moving it to the trash folder.
/// </summary>
/// <param name="MailItem">Item to delete permanently.</param>
public record DeleteRequest(MailCopy MailItem) : MailRequestBase(MailItem),
ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
public override void ApplyUIChanges()
/// <summary>
/// Hard delete request. This request will delete the mail item from the server without moving it to the trash folder.
/// </summary>
/// <param name="MailItem">Item to delete permanently.</param>
public record DeleteRequest(MailCopy MailItem) : MailRequestBase(MailItem),
ICustomFolderSynchronizationRequest
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public class BatchDeleteRequest : BatchCollection<DeleteRequest>
{
public BatchDeleteRequest(IEnumerable<DeleteRequest> collection) : base(collection)
public class BatchDeleteRequest : BatchCollection<DeleteRequest>
{
public BatchDeleteRequest(IEnumerable<DeleteRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,34 +7,35 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item), ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead;
public bool ExcludeMustHaveFolders => true;
public override void ApplyUIChanges()
public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item), ICustomFolderSynchronizationRequest
{
Item.IsRead = IsRead;
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead;
public bool ExcludeMustHaveFolders => true;
public override void ApplyUIChanges()
{
Item.IsRead = IsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
public override void RevertUIChanges()
{
Item.IsRead = !IsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
}
public override void RevertUIChanges()
{
Item.IsRead = !IsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item));
}
}
public class BatchMarkReadRequest : BatchCollection<MarkReadRequest>
{
public BatchMarkReadRequest(IEnumerable<MarkReadRequest> collection) : base(collection)
public class BatchMarkReadRequest : BatchCollection<MarkReadRequest>
{
public BatchMarkReadRequest(IEnumerable<MarkReadRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,29 +7,30 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
public record MoveRequest(MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder)
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
public override void ApplyUIChanges()
public record MoveRequest(MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder)
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public class BatchMoveRequest : BatchCollection<MoveRequest>, IUIChangeRequest
{
public BatchMoveRequest(IEnumerable<MoveRequest> collection) : base(collection)
public class BatchMoveRequest : BatchCollection<MoveRequest>, IUIChangeRequest
{
public BatchMoveRequest(IEnumerable<MoveRequest> collection) : base(collection)
{
}
}
}

View File

@@ -3,16 +3,17 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Requests;
namespace Wino.Core.Requests.Mail;
public record MoveToFocusedRequest(MailCopy Item, bool MoveToFocused) : MailRequestBase(Item)
namespace Wino.Core.Requests.Mail
{
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MoveToFocused;
}
public class BatchMoveToFocusedRequest : BatchCollection<MoveToFocusedRequest>
{
public BatchMoveToFocusedRequest(IEnumerable<MoveToFocusedRequest> collection) : base(collection)
public record MoveToFocusedRequest(MailCopy Item, bool MoveToFocused) : MailRequestBase(Item)
{
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MoveToFocused;
}
public class BatchMoveToFocusedRequest : BatchCollection<MoveToFocusedRequest>
{
public BatchMoveToFocusedRequest(IEnumerable<MoveToFocusedRequest> collection) : base(collection)
{
}
}
}

View File

@@ -7,38 +7,39 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
public record SendDraftRequest(SendDraftPreparationRequest Request)
: MailRequestBase(Request.MailItem),
ICustomFolderSynchronizationRequest
namespace Wino.Core.Requests.Mail
{
public List<Guid> SynchronizationFolderIds
public record SendDraftRequest(SendDraftPreparationRequest Request)
: MailRequestBase(Request.MailItem),
ICustomFolderSynchronizationRequest
{
get
public List<Guid> SynchronizationFolderIds
{
var folderIds = new List<Guid> { Request.DraftFolder.Id };
if (Request.SentFolder != null)
get
{
folderIds.Add(Request.SentFolder.Id);
}
var folderIds = new List<Guid> { Request.DraftFolder.Id };
return folderIds;
if (Request.SentFolder != null)
{
folderIds.Add(Request.SentFolder.Id);
}
return folderIds;
}
}
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Send;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Send;
public override void ApplyUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
}
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(Item));
}
}

View File

@@ -5,31 +5,32 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using IAuthenticationProvider = Wino.Core.Domain.Interfaces.IAuthenticationProvider;
namespace Wino.Core.Services;
public class AuthenticationProvider : IAuthenticationProvider
namespace Wino.Core.Services
{
private readonly INativeAppService _nativeAppService;
private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IAuthenticatorConfig _authenticatorConfig;
public AuthenticationProvider(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig)
public class AuthenticationProvider : IAuthenticationProvider
{
_nativeAppService = nativeAppService;
_applicationConfiguration = applicationConfiguration;
_authenticatorConfig = authenticatorConfig;
}
private readonly INativeAppService _nativeAppService;
private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IAuthenticatorConfig _authenticatorConfig;
public IAuthenticator GetAuthenticator(MailProviderType providerType)
{
// TODO: Move DI
return providerType switch
public AuthenticationProvider(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig)
{
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
};
_nativeAppService = nativeAppService;
_applicationConfiguration = applicationConfiguration;
_authenticatorConfig = authenticatorConfig;
}
public IAuthenticator GetAuthenticator(MailProviderType providerType)
{
// TODO: Move DI
return providerType switch
{
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
};
}
}
}

View File

@@ -7,50 +7,51 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.AutoDiscovery;
namespace Wino.Core.Services;
/// <summary>
/// We have 2 methods to do auto discovery.
/// 1. Use https://emailsettings.firetrust.com/settings?q={address} API
/// 2. TODO: Thunderbird auto discovery file.
/// </summary>
public class AutoDiscoveryService : IAutoDiscoveryService
namespace Wino.Core.Services
{
private const string FiretrustURL = " https://emailsettings.firetrust.com/settings?q=";
// TODO: Try Thunderbird Auto Discovery as second approach.
public Task<AutoDiscoverySettings> GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings)
=> GetSettingsFromFiretrustAsync(autoDiscoveryMinimalSettings.Email);
private static async Task<AutoDiscoverySettings> GetSettingsFromFiretrustAsync(string mailAddress)
/// <summary>
/// We have 2 methods to do auto discovery.
/// 1. Use https://emailsettings.firetrust.com/settings?q={address} API
/// 2. TODO: Thunderbird auto discovery file.
/// </summary>
public class AutoDiscoveryService : IAutoDiscoveryService
{
using var client = new HttpClient();
var response = await client.GetAsync($"{FiretrustURL}{mailAddress}");
private const string FiretrustURL = " https://emailsettings.firetrust.com/settings?q=";
if (response.IsSuccessStatusCode)
return await DeserializeFiretrustResponse(response);
else
// TODO: Try Thunderbird Auto Discovery as second approach.
public Task<AutoDiscoverySettings> GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings)
=> GetSettingsFromFiretrustAsync(autoDiscoveryMinimalSettings.Email);
private static async Task<AutoDiscoverySettings> GetSettingsFromFiretrustAsync(string mailAddress)
{
Log.Warning($"Firetrust AutoDiscovery failed. ({response.StatusCode})");
using var client = new HttpClient();
var response = await client.GetAsync($"{FiretrustURL}{mailAddress}");
if (response.IsSuccessStatusCode)
return await DeserializeFiretrustResponse(response);
else
{
Log.Warning($"Firetrust AutoDiscovery failed. ({response.StatusCode})");
return null;
}
}
private static async Task<AutoDiscoverySettings> DeserializeFiretrustResponse(HttpResponseMessage response)
{
try
{
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(content, DomainModelsJsonContext.Default.AutoDiscoverySettings);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize Firetrust response.");
}
return null;
}
}
private static async Task<AutoDiscoverySettings> DeserializeFiretrustResponse(HttpResponseMessage response)
{
try
{
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(content, DomainModelsJsonContext.Default.AutoDiscoverySettings);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize Firetrust response.");
}
return null;
}
}

View File

@@ -5,53 +5,54 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Connectivity;
using Wino.Core.Integration;
namespace Wino.Core.Services;
public class ImapTestService : IImapTestService
namespace Wino.Core.Services
{
public const string ProtocolLogFileName = "ImapProtocolLog.log";
private readonly IPreferencesService _preferencesService;
private readonly IApplicationConfiguration _appInitializerService;
private Stream _protocolLogStream;
public ImapTestService(IPreferencesService preferencesService, IApplicationConfiguration appInitializerService)
public class ImapTestService : IImapTestService
{
_preferencesService = preferencesService;
_appInitializerService = appInitializerService;
}
public const string ProtocolLogFileName = "ImapProtocolLog.log";
private void EnsureProtocolLogFileExists()
{
// Create new file for protocol logger.
var localAppFolderPath = _appInitializerService.ApplicationDataFolderPath;
private readonly IPreferencesService _preferencesService;
private readonly IApplicationConfiguration _appInitializerService;
var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName);
private Stream _protocolLogStream;
if (File.Exists(logFile))
File.Delete(logFile);
_protocolLogStream = File.Create(logFile);
}
public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake)
{
EnsureProtocolLogFileExists();
var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation, _protocolLogStream);
var clientPool = new ImapClientPool(poolOptions)
public ImapTestService(IPreferencesService preferencesService, IApplicationConfiguration appInitializerService)
{
ThrowOnSSLHandshakeCallback = !allowSSLHandShake
};
_preferencesService = preferencesService;
_appInitializerService = appInitializerService;
}
using (clientPool)
private void EnsureProtocolLogFileExists()
{
// This call will make sure that everything is authenticated + connected successfully.
var client = await clientPool.GetClientAsync();
// Create new file for protocol logger.
var localAppFolderPath = _appInitializerService.ApplicationDataFolderPath;
clientPool.Release(client);
var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName);
if (File.Exists(logFile))
File.Delete(logFile);
_protocolLogStream = File.Create(logFile);
}
public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake)
{
EnsureProtocolLogFileExists();
var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation, _protocolLogStream);
var clientPool = new ImapClientPool(poolOptions)
{
ThrowOnSSLHandshakeCallback = !allowSSLHandShake
};
using (clientPool)
{
// This call will make sure that everything is authenticated + connected successfully.
var client = await clientPool.GetClientAsync();
clientPool.Release(client);
}
}
}
}

View File

@@ -6,120 +6,121 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration.Processors;
using Wino.Core.Synchronizers.Mail;
namespace Wino.Core.Services;
public class SynchronizerFactory : ISynchronizerFactory
namespace Wino.Core.Services
{
private bool isInitialized = false;
private readonly IAccountService _accountService;
private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider;
private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly IImapChangeProcessor _imapChangeProcessor;
private readonly IOutlookAuthenticator _outlookAuthenticator;
private readonly IGmailAuthenticator _gmailAuthenticator;
private readonly List<IWinoSynchronizerBase> synchronizerCache = new();
public SynchronizerFactory(IOutlookChangeProcessor outlookChangeProcessor,
IGmailChangeProcessor gmailChangeProcessor,
IImapChangeProcessor imapChangeProcessor,
IOutlookAuthenticator outlookAuthenticator,
IGmailAuthenticator gmailAuthenticator,
IAccountService accountService,
IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider,
IApplicationConfiguration applicationConfiguration)
public class SynchronizerFactory : ISynchronizerFactory
{
_outlookChangeProcessor = outlookChangeProcessor;
_gmailChangeProcessor = gmailChangeProcessor;
_imapChangeProcessor = imapChangeProcessor;
_outlookAuthenticator = outlookAuthenticator;
_gmailAuthenticator = gmailAuthenticator;
_accountService = accountService;
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
_applicationConfiguration = applicationConfiguration;
}
private bool isInitialized = false;
public async Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId)
{
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
private readonly IAccountService _accountService;
private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider;
private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly IImapChangeProcessor _imapChangeProcessor;
private readonly IOutlookAuthenticator _outlookAuthenticator;
private readonly IGmailAuthenticator _gmailAuthenticator;
if (synchronizer == null)
private readonly List<IWinoSynchronizerBase> synchronizerCache = new();
public SynchronizerFactory(IOutlookChangeProcessor outlookChangeProcessor,
IGmailChangeProcessor gmailChangeProcessor,
IImapChangeProcessor imapChangeProcessor,
IOutlookAuthenticator outlookAuthenticator,
IGmailAuthenticator gmailAuthenticator,
IAccountService accountService,
IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider,
IApplicationConfiguration applicationConfiguration)
{
var account = await _accountService.GetAccountAsync(accountId);
_outlookChangeProcessor = outlookChangeProcessor;
_gmailChangeProcessor = gmailChangeProcessor;
_imapChangeProcessor = imapChangeProcessor;
_outlookAuthenticator = outlookAuthenticator;
_gmailAuthenticator = gmailAuthenticator;
_accountService = accountService;
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
_applicationConfiguration = applicationConfiguration;
}
if (account != null)
public async Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId)
{
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
if (synchronizer == null)
{
synchronizer = CreateNewSynchronizer(account);
var account = await _accountService.GetAccountAsync(accountId);
if (account != null)
{
synchronizer = CreateNewSynchronizer(account);
return await GetAccountSynchronizerAsync(accountId);
return await GetAccountSynchronizerAsync(accountId);
}
}
return synchronizer;
}
return synchronizer;
}
private IWinoSynchronizerBase CreateIntegratorWithDefaultProcessor(MailAccount mailAccount)
{
var providerType = mailAccount.ProviderType;
switch (providerType)
private IWinoSynchronizerBase CreateIntegratorWithDefaultProcessor(MailAccount mailAccount)
{
case Domain.Enums.MailProviderType.Outlook:
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor);
case Domain.Enums.MailProviderType.Gmail:
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor);
case Domain.Enums.MailProviderType.IMAP4:
return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration);
default:
break;
var providerType = mailAccount.ProviderType;
switch (providerType)
{
case Domain.Enums.MailProviderType.Outlook:
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor);
case Domain.Enums.MailProviderType.Gmail:
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor);
case Domain.Enums.MailProviderType.IMAP4:
return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration);
default:
break;
}
return null;
}
return null;
}
public IWinoSynchronizerBase CreateNewSynchronizer(MailAccount account)
{
var synchronizer = CreateIntegratorWithDefaultProcessor(account);
if (synchronizer is IImapSynchronizer imapSynchronizer)
public IWinoSynchronizerBase CreateNewSynchronizer(MailAccount account)
{
// Start the idle client for IMAP synchronizer.
_ = imapSynchronizer.StartIdleClientAsync();
var synchronizer = CreateIntegratorWithDefaultProcessor(account);
if (synchronizer is IImapSynchronizer imapSynchronizer)
{
// Start the idle client for IMAP synchronizer.
_ = imapSynchronizer.StartIdleClientAsync();
}
synchronizerCache.Add(synchronizer);
return synchronizer;
}
synchronizerCache.Add(synchronizer);
return synchronizer;
}
public async Task InitializeAsync()
{
if (isInitialized) return;
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
public async Task InitializeAsync()
{
CreateNewSynchronizer(account);
if (isInitialized) return;
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
CreateNewSynchronizer(account);
}
isInitialized = true;
}
isInitialized = true;
}
public async Task DeleteSynchronizerAsync(Guid accountId)
{
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
if (synchronizer != null)
public async Task DeleteSynchronizerAsync(Guid accountId)
{
// Stop the current synchronization.
await synchronizer.KillSynchronizerAsync();
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
synchronizerCache.Remove(synchronizer);
if (synchronizer != null)
{
// Stop the current synchronization.
await synchronizer.KillSynchronizerAsync();
synchronizerCache.Remove(synchronizer);
}
}
}
}

View File

@@ -2,30 +2,31 @@
using Wino.Core.Domain.Interfaces;
using Wino.Services.Threading;
namespace Wino.Core.Services;
public class ThreadingStrategyProvider : IThreadingStrategyProvider
namespace Wino.Core.Services
{
private readonly OutlookThreadingStrategy _outlookThreadingStrategy;
private readonly GmailThreadingStrategy _gmailThreadingStrategy;
private readonly ImapThreadingStrategy _imapThreadStrategy;
public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy,
GmailThreadingStrategy gmailThreadingStrategy,
ImapThreadingStrategy imapThreadStrategy)
public class ThreadingStrategyProvider : IThreadingStrategyProvider
{
_outlookThreadingStrategy = outlookThreadingStrategy;
_gmailThreadingStrategy = gmailThreadingStrategy;
_imapThreadStrategy = imapThreadStrategy;
}
private readonly OutlookThreadingStrategy _outlookThreadingStrategy;
private readonly GmailThreadingStrategy _gmailThreadingStrategy;
private readonly ImapThreadingStrategy _imapThreadStrategy;
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
{
return mailProviderType switch
public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy,
GmailThreadingStrategy gmailThreadingStrategy,
ImapThreadingStrategy imapThreadStrategy)
{
MailProviderType.Outlook => _outlookThreadingStrategy,
MailProviderType.Gmail => _gmailThreadingStrategy,
_ => _imapThreadStrategy,
};
_outlookThreadingStrategy = outlookThreadingStrategy;
_gmailThreadingStrategy = gmailThreadingStrategy;
_imapThreadStrategy = imapThreadStrategy;
}
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
{
return mailProviderType switch
{
MailProviderType.Outlook => _outlookThreadingStrategy,
MailProviderType.Gmail => _gmailThreadingStrategy,
_ => _imapThreadStrategy,
};
}
}
}

View File

@@ -6,30 +6,31 @@ using Serilog;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Reader;
namespace Wino.Core.Services;
public class UnsubscriptionService : IUnsubscriptionService
namespace Wino.Core.Services
{
public async Task<bool> OneClickUnsubscribeAsync(UnsubscribeInfo info)
public class UnsubscriptionService : IUnsubscriptionService
{
try
public async Task<bool> OneClickUnsubscribeAsync(UnsubscribeInfo info)
{
using var httpClient = new HttpClient();
var unsubscribeRequest = new HttpRequestMessage(HttpMethod.Post, info.HttpLink)
try
{
Content = new StringContent("List-Unsubscribe=One-Click", Encoding.UTF8, "application/x-www-form-urlencoded")
};
using var httpClient = new HttpClient();
var result = await httpClient.SendAsync(unsubscribeRequest).ConfigureAwait(false);
var unsubscribeRequest = new HttpRequestMessage(HttpMethod.Post, info.HttpLink)
{
Content = new StringContent("List-Unsubscribe=One-Click", Encoding.UTF8, "application/x-www-form-urlencoded")
};
return result.IsSuccessStatusCode;
var result = await httpClient.SendAsync(unsubscribeRequest).ConfigureAwait(false);
return result.IsSuccessStatusCode;
}
catch (Exception ex)
{
Log.Error("Failed to unsubscribe from {HttpLink} - {Message}", info.HttpLink, ex.Message);
}
return false;
}
catch (Exception ex)
{
Log.Error("Failed to unsubscribe from {HttpLink} - {Message}", info.HttpLink, ex.Message);
}
return false;
}
}

View File

@@ -14,148 +14,149 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Mail;
using Wino.Messaging.Server;
namespace Wino.Core.Services;
public class WinoRequestDelegator : IWinoRequestDelegator
namespace Wino.Core.Services
{
private readonly IWinoRequestProcessor _winoRequestProcessor;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private readonly IFolderService _folderService;
private readonly IMailDialogService _dialogService;
public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor,
IWinoServerConnectionManager winoServerConnectionManager,
IFolderService folderService,
IMailDialogService dialogService)
public class WinoRequestDelegator : IWinoRequestDelegator
{
_winoRequestProcessor = winoRequestProcessor;
_winoServerConnectionManager = winoServerConnectionManager;
_folderService = folderService;
_dialogService = dialogService;
}
private readonly IWinoRequestProcessor _winoRequestProcessor;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private readonly IFolderService _folderService;
private readonly IMailDialogService _dialogService;
public async Task ExecuteAsync(MailOperationPreperationRequest request)
{
var requests = new List<IMailActionRequest>();
try
public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor,
IWinoServerConnectionManager winoServerConnectionManager,
IFolderService folderService,
IMailDialogService dialogService)
{
requests = await _winoRequestProcessor.PrepareRequestsAsync(request);
}
catch (UnavailableSpecialFolderException unavailableSpecialFolderException)
{
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle,
string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType),
InfoBarMessageType.Warning,
Translator.SettingConfigureSpecialFolders_Button,
() =>
{
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
});
}
catch (InvalidMoveTargetException)
{
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Info_InvalidMoveTargetMessage, InfoBarMessageType.Warning);
}
catch (NotImplementedException)
{
_dialogService.ShowNotSupportedMessage();
}
catch (Exception ex)
{
Log.Error(ex, "Request creation failed.");
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
_winoRequestProcessor = winoRequestProcessor;
_winoServerConnectionManager = winoServerConnectionManager;
_folderService = folderService;
_dialogService = dialogService;
}
if (requests == null || !requests.Any()) return;
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
// Queue requests for each account and start synchronization.
foreach (var accountId in accountIds)
public async Task ExecuteAsync(MailOperationPreperationRequest request)
{
foreach (var accountRequest in accountId)
var requests = new List<IMailActionRequest>();
try
{
await QueueRequestAsync(accountRequest, accountId.Key);
requests = await _winoRequestProcessor.PrepareRequestsAsync(request);
}
catch (UnavailableSpecialFolderException unavailableSpecialFolderException)
{
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle,
string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType),
InfoBarMessageType.Warning,
Translator.SettingConfigureSpecialFolders_Button,
() =>
{
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
});
}
catch (InvalidMoveTargetException)
{
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Info_InvalidMoveTargetMessage, InfoBarMessageType.Warning);
}
catch (NotImplementedException)
{
_dialogService.ShowNotSupportedMessage();
}
catch (Exception ex)
{
Log.Error(ex, "Request creation failed.");
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
}
await QueueSynchronizationAsync(accountId.Key);
if (requests == null || !requests.Any()) return;
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
// Queue requests for each account and start synchronization.
foreach (var accountId in accountIds)
{
foreach (var accountRequest in accountId)
{
await QueueRequestAsync(accountRequest, accountId.Key);
}
await QueueSynchronizationAsync(accountId.Key);
}
}
}
public async Task ExecuteAsync(FolderOperationPreperationRequest folderRequest)
{
if (folderRequest == null || folderRequest.Folder == null) return;
IRequestBase request = null;
var accountId = folderRequest.Folder.MailAccountId;
try
public async Task ExecuteAsync(FolderOperationPreperationRequest folderRequest)
{
request = await _winoRequestProcessor.PrepareFolderRequestAsync(folderRequest);
if (folderRequest == null || folderRequest.Folder == null) return;
IRequestBase request = null;
var accountId = folderRequest.Folder.MailAccountId;
try
{
request = await _winoRequestProcessor.PrepareFolderRequestAsync(folderRequest);
}
catch (NotImplementedException)
{
_dialogService.ShowNotSupportedMessage();
}
catch (Exception ex)
{
Log.Error(ex, "Folder operation execution failed.");
}
if (request == null) return;
await QueueRequestAsync(request, accountId);
await QueueSynchronizationAsync(accountId);
}
catch (NotImplementedException)
public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest)
{
_dialogService.ShowNotSupportedMessage();
var request = new CreateDraftRequest(draftPreperationRequest);
await QueueRequestAsync(request, draftPreperationRequest.Account.Id);
await QueueSynchronizationAsync(draftPreperationRequest.Account.Id);
}
catch (Exception ex)
public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest)
{
Log.Error(ex, "Folder operation execution failed.");
var request = new SendDraftRequest(sendDraftPreperationRequest);
await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
await QueueSynchronizationAsync(sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
}
if (request == null) return;
private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
try
{
await EnsureServerConnectedAsync();
await _winoServerConnectionManager.QueueRequestAsync(request, accountId);
}
catch (WinoServerException serverException)
{
_dialogService.InfoBarMessage("Wino Server Exception", serverException.Message, InfoBarMessageType.Error);
}
}
await QueueRequestAsync(request, accountId);
await QueueSynchronizationAsync(accountId);
}
public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest)
{
var request = new CreateDraftRequest(draftPreperationRequest);
await QueueRequestAsync(request, draftPreperationRequest.Account.Id);
await QueueSynchronizationAsync(draftPreperationRequest.Account.Id);
}
public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest)
{
var request = new SendDraftRequest(sendDraftPreperationRequest);
await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
await QueueSynchronizationAsync(sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
}
private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
try
private async Task QueueSynchronizationAsync(Guid accountId)
{
await EnsureServerConnectedAsync();
await _winoServerConnectionManager.QueueRequestAsync(request, accountId);
var options = new MailSynchronizationOptions()
{
AccountId = accountId,
Type = MailSynchronizationType.ExecuteRequests
};
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
}
catch (WinoServerException serverException)
private async Task EnsureServerConnectedAsync()
{
_dialogService.InfoBarMessage("Wino Server Exception", serverException.Message, InfoBarMessageType.Error);
if (_winoServerConnectionManager.Status == WinoServerConnectionStatus.Connected) return;
await _winoServerConnectionManager.ConnectAsync();
}
}
private async Task QueueSynchronizationAsync(Guid accountId)
{
await EnsureServerConnectedAsync();
var options = new MailSynchronizationOptions()
{
AccountId = accountId,
Type = MailSynchronizationType.ExecuteRequests
};
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
}
private async Task EnsureServerConnectedAsync()
{
if (_winoServerConnectionManager.Status == WinoServerConnectionStatus.Connected) return;
await _winoServerConnectionManager.ConnectAsync();
}
}

View File

@@ -12,255 +12,256 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Requests.Folder;
using Wino.Core.Requests.Mail;
namespace Wino.Core.Services;
/// <summary>
/// Intermediary processor for converting a user action to executable Wino requests.
/// Primarily responsible for batching requests by AccountId and FolderId.
/// </summary>
public class WinoRequestProcessor : IWinoRequestProcessor
namespace Wino.Core.Services
{
private readonly IFolderService _folderService;
private readonly IKeyPressService _keyPressService;
private readonly IPreferencesService _preferencesService;
private readonly IMailDialogService _dialogService;
private readonly IMailService _mailService;
/// <summary>
/// Set of rules that defines which action should be executed if user wants to toggle an action.
/// Intermediary processor for converting a user action to executable Wino requests.
/// Primarily responsible for batching requests by AccountId and FolderId.
/// </summary>
private readonly List<ToggleRequestRule> _toggleRequestRules =
[
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)),
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, bool>((item) => !item.IsRead)),
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<IMailItem, bool>((item) => item.IsFlagged)),
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)),
];
public WinoRequestProcessor(IFolderService folderService,
IKeyPressService keyPressService,
IPreferencesService preferencesService,
IMailDialogService dialogService,
IMailService mailService)
public class WinoRequestProcessor : IWinoRequestProcessor
{
_folderService = folderService;
_keyPressService = keyPressService;
_preferencesService = preferencesService;
_dialogService = dialogService;
_mailService = mailService;
}
private readonly IFolderService _folderService;
private readonly IKeyPressService _keyPressService;
private readonly IPreferencesService _preferencesService;
private readonly IMailDialogService _dialogService;
private readonly IMailService _mailService;
public async Task<List<IMailActionRequest>> PrepareRequestsAsync(MailOperationPreperationRequest preperationRequest)
{
var action = preperationRequest.Action;
var moveTargetStructure = preperationRequest.MoveTargetFolder;
/// <summary>
/// Set of rules that defines which action should be executed if user wants to toggle an action.
/// </summary>
private readonly List<ToggleRequestRule> _toggleRequestRules =
[
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)),
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, bool>((item) => !item.IsRead)),
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<IMailItem, bool>((item) => item.IsFlagged)),
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)),
];
// Ask confirmation for permanent delete operation.
// Drafts are always hard deleted without any protection.
if (!preperationRequest.IgnoreHardDeleteProtection && ((action == MailOperation.SoftDelete && _keyPressService.IsShiftKeyPressed()) || action == MailOperation.HardDelete))
public WinoRequestProcessor(IFolderService folderService,
IKeyPressService keyPressService,
IPreferencesService preferencesService,
IMailDialogService dialogService,
IMailService mailService)
{
if (_preferencesService.IsHardDeleteProtectionEnabled)
_folderService = folderService;
_keyPressService = keyPressService;
_preferencesService = preferencesService;
_dialogService = dialogService;
_mailService = mailService;
}
public async Task<List<IMailActionRequest>> PrepareRequestsAsync(MailOperationPreperationRequest preperationRequest)
{
var action = preperationRequest.Action;
var moveTargetStructure = preperationRequest.MoveTargetFolder;
// Ask confirmation for permanent delete operation.
// Drafts are always hard deleted without any protection.
if (!preperationRequest.IgnoreHardDeleteProtection && ((action == MailOperation.SoftDelete && _keyPressService.IsShiftKeyPressed()) || action == MailOperation.HardDelete))
{
var shouldDelete = await _dialogService.ShowHardDeleteConfirmationAsync();
if (!shouldDelete) return default;
}
action = MailOperation.HardDelete;
}
// Make sure there is a move target folder if action is move.
// Let user pick a folder to move from the dialog.
if (action == MailOperation.Move && moveTargetStructure == null)
{
// TODO: Handle multiple accounts for move operation.
// What happens if we move 2 different mails from 2 different accounts?
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
moveTargetStructure = await _dialogService.PickFolderAsync(accountId, PickFolderReason.Move, _folderService);
if (moveTargetStructure == null)
return default;
}
var requests = new List<IMailActionRequest>();
// TODO: Fix: Collection was modified; enumeration operation may not execute
foreach (var item in preperationRequest.MailItems)
{
var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
if (singleRequest == null) continue;
requests.Add(singleRequest);
}
return requests;
}
private async Task<IMailActionRequest> GetSingleRequestAsync(MailCopy mailItem, MailOperation action, IMailItemFolder moveTargetStructure, bool shouldToggleActions)
{
if (mailItem.AssignedAccount == null) throw new ArgumentException(Translator.Exception_NullAssignedAccount);
if (mailItem.AssignedFolder == null) throw new ArgumentException(Translator.Exception_NullAssignedFolder);
// Rule: Soft deletes from Trash folder must perform Hard Delete.
if (action == MailOperation.SoftDelete && mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Deleted)
action = MailOperation.HardDelete;
// Rule: SoftDelete draft items must be performed as hard delete.
if (action == MailOperation.SoftDelete && mailItem.IsDraft)
action = MailOperation.HardDelete;
// Rule: Soft/Hard deletes on local drafts are always discard local draft.
if ((action == MailOperation.SoftDelete || action == MailOperation.HardDelete) && mailItem.IsLocalDraft)
action = MailOperation.DiscardLocalDraft;
// Rule: Toggle actions must be reverted if ToggleExecution is passed true.
if (shouldToggleActions)
{
var toggleRule = _toggleRequestRules.Find(a => a.SourceAction == action);
if (toggleRule != null && toggleRule.Condition(mailItem))
{
action = toggleRule.TargetAction;
}
}
if (action == MailOperation.MarkAsRead)
return new MarkReadRequest(mailItem, true);
else if (action == MailOperation.MarkAsUnread)
return new MarkReadRequest(mailItem, false);
else if (action == MailOperation.SetFlag)
return new ChangeFlagRequest(mailItem, true);
else if (action == MailOperation.ClearFlag)
return new ChangeFlagRequest(mailItem, false);
else if (action == MailOperation.HardDelete)
return new DeleteRequest(mailItem);
else if (action == MailOperation.Move)
{
if (moveTargetStructure == null)
throw new InvalidMoveTargetException();
// TODO
// Rule: You can't move items to non-move target folders;
// Rule: You can't move items from a folder to itself.
//if (!moveTargetStructure.IsMoveTarget || moveTargetStructure.FolderId == mailItem.AssignedFolder.Id)
// throw new InvalidMoveTargetException();
var pickedFolderItem = await _folderService.GetFolderAsync(moveTargetStructure.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, pickedFolderItem);
}
else if (action == MailOperation.Archive)
{
// For IMAP and Outlook: Validate archive folder exists.
// Gmail doesn't need archive folder existence.
MailItemFolder archiveFolder = null;
bool shouldRequireArchiveFolder = mailItem.AssignedAccount.ProviderType == MailProviderType.Outlook
|| mailItem.AssignedAccount.ProviderType == MailProviderType.IMAP4;
if (shouldRequireArchiveFolder)
{
archiveFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Archive)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Archive, mailItem.AssignedAccount.Id);
}
return new ArchiveRequest(true, mailItem, mailItem.AssignedFolder, archiveFolder);
}
else if (action == MailOperation.MarkAsNotJunk)
{
var inboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Inbox)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Inbox, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, inboxFolder);
}
else if (action == MailOperation.UnArchive)
{
var inboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Inbox)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Inbox, mailItem.AssignedAccount.Id);
return new ArchiveRequest(false, mailItem, mailItem.AssignedFolder, inboxFolder);
}
else if (action == MailOperation.SoftDelete)
{
var trashFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Deleted)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Deleted, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, trashFolder);
}
else if (action == MailOperation.MoveToJunk)
{
var junkFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Junk)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Junk, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, junkFolder);
}
else if (action == MailOperation.AlwaysMoveToFocused || action == MailOperation.AlwaysMoveToOther)
return new AlwaysMoveToRequest(mailItem, action == MailOperation.AlwaysMoveToFocused);
else if (action == MailOperation.DiscardLocalDraft)
await _mailService.DeleteMailAsync(mailItem.AssignedAccount.Id, mailItem.Id);
else
throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedAction, action));
return null;
}
public async Task<IFolderActionRequest> PrepareFolderRequestAsync(FolderOperationPreperationRequest request)
{
if (request == null || request.Folder == null) return default;
IFolderActionRequest change = null;
var folder = request.Folder;
var operation = request.Action;
switch (request.Action)
{
case FolderOperation.Pin:
case FolderOperation.Unpin:
await _folderService.ChangeStickyStatusAsync(folder.Id, operation == FolderOperation.Pin);
break;
case FolderOperation.Rename:
var newFolderName = await _dialogService.ShowTextInputDialogAsync(folder.FolderName, Translator.DialogMessage_RenameFolderTitle, Translator.DialogMessage_RenameFolderMessage, Translator.FolderOperation_Rename);
if (!string.IsNullOrEmpty(newFolderName))
if (_preferencesService.IsHardDeleteProtectionEnabled)
{
change = new RenameFolderRequest(folder, folder.FolderName, newFolderName);
var shouldDelete = await _dialogService.ShowHardDeleteConfirmationAsync();
if (!shouldDelete) return default;
}
break;
case FolderOperation.Empty:
var mailsToDelete = await _mailService.GetMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
action = MailOperation.HardDelete;
}
change = new EmptyFolderRequest(folder, mailsToDelete);
// Make sure there is a move target folder if action is move.
// Let user pick a folder to move from the dialog.
break;
case FolderOperation.MarkAllAsRead:
if (action == MailOperation.Move && moveTargetStructure == null)
{
// TODO: Handle multiple accounts for move operation.
// What happens if we move 2 different mails from 2 different accounts?
var unreadItems = await _mailService.GetUnreadMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
if (unreadItems.Any())
change = new MarkFolderAsReadRequest(folder, unreadItems);
moveTargetStructure = await _dialogService.PickFolderAsync(accountId, PickFolderReason.Move, _folderService);
break;
//case FolderOperation.Delete:
// var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete.");
if (moveTargetStructure == null)
return default;
}
// if (isConfirmed)
// change = new DeleteFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId);
var requests = new List<IMailActionRequest>();
// break;
//default:
// throw new NotImplementedException();
// TODO: Fix: Collection was modified; enumeration operation may not execute
foreach (var item in preperationRequest.MailItems)
{
var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
if (singleRequest == null) continue;
requests.Add(singleRequest);
}
return requests;
}
return change;
private async Task<IMailActionRequest> GetSingleRequestAsync(MailCopy mailItem, MailOperation action, IMailItemFolder moveTargetStructure, bool shouldToggleActions)
{
if (mailItem.AssignedAccount == null) throw new ArgumentException(Translator.Exception_NullAssignedAccount);
if (mailItem.AssignedFolder == null) throw new ArgumentException(Translator.Exception_NullAssignedFolder);
// Rule: Soft deletes from Trash folder must perform Hard Delete.
if (action == MailOperation.SoftDelete && mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Deleted)
action = MailOperation.HardDelete;
// Rule: SoftDelete draft items must be performed as hard delete.
if (action == MailOperation.SoftDelete && mailItem.IsDraft)
action = MailOperation.HardDelete;
// Rule: Soft/Hard deletes on local drafts are always discard local draft.
if ((action == MailOperation.SoftDelete || action == MailOperation.HardDelete) && mailItem.IsLocalDraft)
action = MailOperation.DiscardLocalDraft;
// Rule: Toggle actions must be reverted if ToggleExecution is passed true.
if (shouldToggleActions)
{
var toggleRule = _toggleRequestRules.Find(a => a.SourceAction == action);
if (toggleRule != null && toggleRule.Condition(mailItem))
{
action = toggleRule.TargetAction;
}
}
if (action == MailOperation.MarkAsRead)
return new MarkReadRequest(mailItem, true);
else if (action == MailOperation.MarkAsUnread)
return new MarkReadRequest(mailItem, false);
else if (action == MailOperation.SetFlag)
return new ChangeFlagRequest(mailItem, true);
else if (action == MailOperation.ClearFlag)
return new ChangeFlagRequest(mailItem, false);
else if (action == MailOperation.HardDelete)
return new DeleteRequest(mailItem);
else if (action == MailOperation.Move)
{
if (moveTargetStructure == null)
throw new InvalidMoveTargetException();
// TODO
// Rule: You can't move items to non-move target folders;
// Rule: You can't move items from a folder to itself.
//if (!moveTargetStructure.IsMoveTarget || moveTargetStructure.FolderId == mailItem.AssignedFolder.Id)
// throw new InvalidMoveTargetException();
var pickedFolderItem = await _folderService.GetFolderAsync(moveTargetStructure.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, pickedFolderItem);
}
else if (action == MailOperation.Archive)
{
// For IMAP and Outlook: Validate archive folder exists.
// Gmail doesn't need archive folder existence.
MailItemFolder archiveFolder = null;
bool shouldRequireArchiveFolder = mailItem.AssignedAccount.ProviderType == MailProviderType.Outlook
|| mailItem.AssignedAccount.ProviderType == MailProviderType.IMAP4;
if (shouldRequireArchiveFolder)
{
archiveFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Archive)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Archive, mailItem.AssignedAccount.Id);
}
return new ArchiveRequest(true, mailItem, mailItem.AssignedFolder, archiveFolder);
}
else if (action == MailOperation.MarkAsNotJunk)
{
var inboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Inbox)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Inbox, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, inboxFolder);
}
else if (action == MailOperation.UnArchive)
{
var inboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Inbox)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Inbox, mailItem.AssignedAccount.Id);
return new ArchiveRequest(false, mailItem, mailItem.AssignedFolder, inboxFolder);
}
else if (action == MailOperation.SoftDelete)
{
var trashFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Deleted)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Deleted, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, trashFolder);
}
else if (action == MailOperation.MoveToJunk)
{
var junkFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Junk)
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Junk, mailItem.AssignedAccount.Id);
return new MoveRequest(mailItem, mailItem.AssignedFolder, junkFolder);
}
else if (action == MailOperation.AlwaysMoveToFocused || action == MailOperation.AlwaysMoveToOther)
return new AlwaysMoveToRequest(mailItem, action == MailOperation.AlwaysMoveToFocused);
else if (action == MailOperation.DiscardLocalDraft)
await _mailService.DeleteMailAsync(mailItem.AssignedAccount.Id, mailItem.Id);
else
throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedAction, action));
return null;
}
public async Task<IFolderActionRequest> PrepareFolderRequestAsync(FolderOperationPreperationRequest request)
{
if (request == null || request.Folder == null) return default;
IFolderActionRequest change = null;
var folder = request.Folder;
var operation = request.Action;
switch (request.Action)
{
case FolderOperation.Pin:
case FolderOperation.Unpin:
await _folderService.ChangeStickyStatusAsync(folder.Id, operation == FolderOperation.Pin);
break;
case FolderOperation.Rename:
var newFolderName = await _dialogService.ShowTextInputDialogAsync(folder.FolderName, Translator.DialogMessage_RenameFolderTitle, Translator.DialogMessage_RenameFolderMessage, Translator.FolderOperation_Rename);
if (!string.IsNullOrEmpty(newFolderName))
{
change = new RenameFolderRequest(folder, folder.FolderName, newFolderName);
}
break;
case FolderOperation.Empty:
var mailsToDelete = await _mailService.GetMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
change = new EmptyFolderRequest(folder, mailsToDelete);
break;
case FolderOperation.MarkAllAsRead:
var unreadItems = await _mailService.GetUnreadMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
if (unreadItems.Any())
change = new MarkFolderAsReadRequest(folder, unreadItems);
break;
//case FolderOperation.Delete:
// var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete.");
// if (isConfirmed)
// change = new DeleteFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId);
// break;
//default:
// throw new NotImplementedException();
}
return change;
}
}
}

View File

@@ -11,98 +11,99 @@ using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Requests.Bundles;
using Wino.Messaging.UI;
namespace Wino.Core.Synchronizers;
public abstract class BaseSynchronizer<TBaseRequest> : IBaseSynchronizer
namespace Wino.Core.Synchronizers
{
protected SemaphoreSlim synchronizationSemaphore = new(1);
protected CancellationToken activeSynchronizationCancellationToken;
protected List<IRequestBase> changeRequestQueue = [];
public MailAccount Account { get; }
private AccountSynchronizerState state;
public AccountSynchronizerState State
public abstract class BaseSynchronizer<TBaseRequest> : IBaseSynchronizer
{
get { return state; }
set
protected SemaphoreSlim synchronizationSemaphore = new(1);
protected CancellationToken activeSynchronizationCancellationToken;
protected List<IRequestBase> changeRequestQueue = [];
public MailAccount Account { get; }
private AccountSynchronizerState state;
public AccountSynchronizerState State
{
state = value;
WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value));
}
}
protected BaseSynchronizer(MailAccount account)
{
Account = account;
}
/// <summary>
/// Queues a single request to be executed in the next synchronization.
/// </summary>
/// <param name="request">Request to execute.</param>
public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request);
/// <summary>
/// Runs existing queued requests in the queue.
/// </summary>
/// <param name="batchedRequests">Batched requests to execute. Integrator methods will only receive batched requests.</param>
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// </summary>
protected async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
var profileInformation = await GetProfileInformationAsync();
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
get { return state; }
set
{
Account.Address = profileInformation.AccountAddress;
state = value;
WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value));
}
}
return profileInformation;
}
protected BaseSynchronizer(MailAccount account)
{
Account = account;
}
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
{
using var client = new HttpClient();
/// <summary>
/// Queues a single request to be executed in the next synchronization.
/// </summary>
/// <param name="request">Request to execute.</param>
public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request);
var response = await client.GetAsync(url).ConfigureAwait(false);
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
/// <summary>
/// Runs existing queued requests in the queue.
/// </summary>
/// <param name="batchedRequests">Batched requests to execute. Integrator methods will only receive batched requests.</param>
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
return Convert.ToBase64String(byteContent);
}
/// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
public List<IRequestBundle<TBaseRequest>> ForEachRequest<TWinoRequestType>(IEnumerable<TWinoRequestType> requests,
Func<TWinoRequestType, TBaseRequest> action)
where TWinoRequestType : IRequestBase
{
List<IRequestBundle<TBaseRequest>> ret = [];
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// </summary>
protected async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
var profileInformation = await GetProfileInformationAsync();
foreach (var request in requests)
ret.Add(new HttpRequestBundle<TBaseRequest>(action(request), request, request));
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
return ret;
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
{
Account.Address = profileInformation.AccountAddress;
}
}
return profileInformation;
}
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url).ConfigureAwait(false);
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
return Convert.ToBase64String(byteContent);
}
public List<IRequestBundle<TBaseRequest>> ForEachRequest<TWinoRequestType>(IEnumerable<TWinoRequestType> requests,
Func<TWinoRequestType, TBaseRequest> action)
where TWinoRequestType : IRequestBase
{
List<IRequestBundle<TBaseRequest>> ret = [];
foreach (var request in requests)
ret.Add(new HttpRequestBundle<TBaseRequest>(action(request), request, request));
return ret;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,119 +12,120 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Core.Synchronizers.ImapSync;
/// <summary>
/// RFC 4551 CONDSTORE IMAP Synchronization strategy.
/// </summary>
internal class CondstoreSynchronizer : ImapSynchronizationStrategyBase
namespace Wino.Core.Synchronizers.ImapSync
{
public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
/// <summary>
/// RFC 4551 CONDSTORE IMAP Synchronization strategy.
/// </summary>
internal class CondstoreSynchronizer : ImapSynchronizationStrategyBase
{
}
public async override Task<List<string>> HandleSynchronizationAsync(IImapClient client,
MailItemFolder folder,
IImapSynchronizer synchronizer,
CancellationToken cancellationToken = default)
{
if (client is not WinoImapClient winoClient)
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
if (!client.Capabilities.HasFlag(ImapCapabilities.CondStore))
throw new ImapSynchronizerStrategyException("Server does not support CONDSTORE.");
IMailFolder remoteFolder = null;
var downloadedMessageIds = new List<string>();
try
public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
{
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
}
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
public async override Task<List<string>> HandleSynchronizationAsync(IImapClient client,
MailItemFolder folder,
IImapSynchronizer synchronizer,
CancellationToken cancellationToken = default)
{
if (client is not WinoImapClient winoClient)
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
var localHighestModSeq = (ulong)folder.HighestModeSeq;
if (!client.Capabilities.HasFlag(ImapCapabilities.CondStore))
throw new ImapSynchronizerStrategyException("Server does not support CONDSTORE.");
bool isInitialSynchronization = localHighestModSeq == 0;
IMailFolder remoteFolder = null;
// There are some changes on new messages or flag changes.
// Deletions are tracked separately because some servers do not increase
// the MODSEQ value for deleted messages.
if (remoteFolder.HighestModSeq > localHighestModSeq)
var downloadedMessageIds = new List<string>();
try
{
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
// Get locally exists mails for the returned UIDs.
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
var localHighestModSeq = (ulong)folder.HighestModeSeq;
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
bool isInitialSynchronization = localHighestModSeq == 0;
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
return downloadedMessageIds;
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
{
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder != null)
// There are some changes on new messages or flag changes.
// Deletions are tracked separately because some servers do not increase
// the MODSEQ value for deleted messages.
if (remoteFolder.HighestModSeq > localHighestModSeq)
{
if (remoteFolder.IsOpen)
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
// Get locally exists mails for the returned UIDs.
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
return downloadedMessageIds;
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
{
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder != null)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
if (remoteFolder.IsOpen)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
}
}
}
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient winoClient, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
var remoteHighestModSeq = remoteFolder.HighestModSeq;
// Search for emails with a MODSEQ greater than the last known value.
// Use SORT extension if server supports.
IList<UniqueId> changedUids = null;
if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort))
{
// Highest mod seq must be greater than 0 for SORT.
changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(Math.Max(localHighestModSeq, 1)), [OrderBy.ReverseDate], cancellationToken).ConfigureAwait(false);
}
else
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient winoClient, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
var remoteHighestModSeq = remoteFolder.HighestModSeq;
// Search for emails with a MODSEQ greater than the last known value.
// Use SORT extension if server supports.
IList<UniqueId> changedUids = null;
if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort))
{
// Highest mod seq must be greater than 0 for SORT.
changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(Math.Max(localHighestModSeq, 1)), [OrderBy.ReverseDate], cancellationToken).ConfigureAwait(false);
}
else
{
changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
}
changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
// For initial synchronizations, take the first allowed number of items.
// For consequtive synchronizations, take all the items. We don't want to miss any changes.
// Smaller uid means newer message. For initial sync, we need start taking items from the top.
bool isInitialSynchronization = localHighestModSeq == 0;
if (isInitialSynchronization)
{
changedUids = changedUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList();
}
return changedUids;
}
changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
// For initial synchronizations, take the first allowed number of items.
// For consequtive synchronizations, take all the items. We don't want to miss any changes.
// Smaller uid means newer message. For initial sync, we need start taking items from the top.
bool isInitialSynchronization = localHighestModSeq == 0;
if (isInitialSynchronization)
{
changedUids = changedUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList();
}
return changedUids;
}
}

View File

@@ -13,172 +13,173 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Services.Extensions;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Core.Synchronizers.ImapSync;
public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrategy
namespace Wino.Core.Synchronizers.ImapSync
{
// Minimum summary items to Fetch for mail synchronization from IMAP.
protected readonly MessageSummaryItems MailSynchronizationFlags =
MessageSummaryItems.Flags |
MessageSummaryItems.UniqueId |
MessageSummaryItems.ThreadId |
MessageSummaryItems.EmailId |
MessageSummaryItems.Headers |
MessageSummaryItems.PreviewText |
MessageSummaryItems.GMailThreadId |
MessageSummaryItems.References |
MessageSummaryItems.ModSeq;
protected IFolderService FolderService { get; }
protected IMailService MailService { get; }
protected MailItemFolder Folder { get; set; }
protected ImapSynchronizationStrategyBase(IFolderService folderService, IMailService mailService)
public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrategy
{
FolderService = folderService;
MailService = mailService;
}
// Minimum summary items to Fetch for mail synchronization from IMAP.
protected readonly MessageSummaryItems MailSynchronizationFlags =
MessageSummaryItems.Flags |
MessageSummaryItems.UniqueId |
MessageSummaryItems.ThreadId |
MessageSummaryItems.EmailId |
MessageSummaryItems.Headers |
MessageSummaryItems.PreviewText |
MessageSummaryItems.GMailThreadId |
MessageSummaryItems.References |
MessageSummaryItems.ModSeq;
public abstract Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
internal abstract Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
protected IFolderService FolderService { get; }
protected IMailService MailService { get; }
protected MailItemFolder Folder { get; set; }
protected async Task<List<string>> HandleChangedUIdsAsync(IImapSynchronizer synchronizer, IMailFolder remoteFolder, IList<UniqueId> changedUids, CancellationToken cancellationToken)
{
List<string> downloadedMessageIds = new();
var existingMails = await MailService.GetExistingMailsAsync(Folder.Id, changedUids).ConfigureAwait(false);
var existingMailUids = existingMails.Select(m => MailkitClientExtensions.ResolveUidStruct(m.Id)).ToArray();
// These are the non-existing mails. They will be downloaded + processed.
var newMessageIds = changedUids.Except(existingMailUids).ToList();
var deletedMessageIds = existingMailUids.Except(changedUids).ToList();
// Fetch minimum data for the existing mails in one query.
var existingFlagData = await remoteFolder.FetchAsync(existingMailUids, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId).ConfigureAwait(false);
foreach (var update in existingFlagData)
protected ImapSynchronizationStrategyBase(IFolderService folderService, IMailService mailService)
{
if (update.UniqueId == null)
{
Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed.");
continue;
}
if (update.Flags == null)
{
Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed.");
continue;
}
var existingMail = existingMails.FirstOrDefault(m => MailkitClientExtensions.ResolveUidStruct(m.Id).Id == update.UniqueId.Id);
if (existingMail == null)
{
Log.Warning($"Couldn't find the mail with UID {update.UniqueId.Id} in the local database. Flag update is ignored.");
continue;
}
await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false);
FolderService = folderService;
MailService = mailService;
}
// Fetch the new mails in batch.
public abstract Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
internal abstract Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
var batchedMessageIds = newMessageIds.Batch(50);
foreach (var group in batchedMessageIds)
protected async Task<List<string>> HandleChangedUIdsAsync(IImapSynchronizer synchronizer, IMailFolder remoteFolder, IList<UniqueId> changedUids, CancellationToken cancellationToken)
{
var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
List<string> downloadedMessageIds = new();
foreach (var summary in summaries)
var existingMails = await MailService.GetExistingMailsAsync(Folder.Id, changedUids).ConfigureAwait(false);
var existingMailUids = existingMails.Select(m => MailkitClientExtensions.ResolveUidStruct(m.Id)).ToArray();
// These are the non-existing mails. They will be downloaded + processed.
var newMessageIds = changedUids.Except(existingMailUids).ToList();
var deletedMessageIds = existingMailUids.Except(changedUids).ToList();
// Fetch minimum data for the existing mails in one query.
var existingFlagData = await remoteFolder.FetchAsync(existingMailUids, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId).ConfigureAwait(false);
foreach (var update in existingFlagData)
{
var mimeMessage = await remoteFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false);
var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage);
var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, Folder, cancellationToken).ConfigureAwait(false);
if (mailPackages != null)
if (update.UniqueId == null)
{
foreach (var package in mailPackages)
Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed.");
continue;
}
if (update.Flags == null)
{
Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed.");
continue;
}
var existingMail = existingMails.FirstOrDefault(m => MailkitClientExtensions.ResolveUidStruct(m.Id).Id == update.UniqueId.Id);
if (existingMail == null)
{
Log.Warning($"Couldn't find the mail with UID {update.UniqueId.Id} in the local database. Flag update is ignored.");
continue;
}
await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false);
}
// Fetch the new mails in batch.
var batchedMessageIds = newMessageIds.Batch(50);
foreach (var group in batchedMessageIds)
{
var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
foreach (var summary in summaries)
{
var mimeMessage = await remoteFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false);
var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage);
var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, Folder, cancellationToken).ConfigureAwait(false);
if (mailPackages != null)
{
// Local draft is mapped. We don't need to create a new mail copy.
if (package == null) continue;
foreach (var package in mailPackages)
{
// Local draft is mapped. We don't need to create a new mail copy.
if (package == null) continue;
bool isCreatedNew = await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
bool isCreatedNew = await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
// This is upsert. We are not interested in updated mails.
if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
// This is upsert. We are not interested in updated mails.
if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
}
}
}
}
return downloadedMessageIds;
}
return downloadedMessageIds;
}
protected async Task HandleMessageFlagsChangeAsync(UniqueId? uniqueId, MessageFlags flags)
{
if (Folder == null) return;
if (uniqueId == null) return;
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Value.Id);
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
var isRead = MailkitClientExtensions.GetIsRead(flags);
await MailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
await MailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
}
protected async Task HandleMessageFlagsChangeAsync(MailCopy mailCopy, MessageFlags flags)
{
if (mailCopy == null) return;
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
var isRead = MailkitClientExtensions.GetIsRead(flags);
if (isFlagged != mailCopy.IsFlagged)
protected async Task HandleMessageFlagsChangeAsync(UniqueId? uniqueId, MessageFlags flags)
{
await MailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
if (Folder == null) return;
if (uniqueId == null) return;
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Value.Id);
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
var isRead = MailkitClientExtensions.GetIsRead(flags);
await MailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
await MailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
}
if (isRead != mailCopy.IsRead)
protected async Task HandleMessageFlagsChangeAsync(MailCopy mailCopy, MessageFlags flags)
{
await MailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
if (mailCopy == null) return;
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
var isRead = MailkitClientExtensions.GetIsRead(flags);
if (isFlagged != mailCopy.IsFlagged)
{
await MailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
}
if (isRead != mailCopy.IsRead)
{
await MailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
}
}
}
protected async Task HandleMessageDeletedAsync(IList<UniqueId> uniqueIds)
{
if (Folder == null) return;
if (uniqueIds == null || uniqueIds.Count == 0) return;
foreach (var uniqueId in uniqueIds)
protected async Task HandleMessageDeletedAsync(IList<UniqueId> uniqueIds)
{
if (uniqueId == null) continue;
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Id);
if (Folder == null) return;
if (uniqueIds == null || uniqueIds.Count == 0) return;
await MailService.DeleteMailAsync(Folder.MailAccountId, localMailCopyId).ConfigureAwait(false);
foreach (var uniqueId in uniqueIds)
{
if (uniqueId == null) continue;
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Id);
await MailService.DeleteMailAsync(Folder.MailAccountId, localMailCopyId).ConfigureAwait(false);
}
}
}
protected void OnMessagesVanished(object sender, MessagesVanishedEventArgs args)
=> HandleMessageDeletedAsync(args.UniqueIds).ConfigureAwait(false);
protected void OnMessagesVanished(object sender, MessagesVanishedEventArgs args)
=> HandleMessageDeletedAsync(args.UniqueIds).ConfigureAwait(false);
protected void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args)
=> HandleMessageFlagsChangeAsync(args.UniqueId, args.Flags).ConfigureAwait(false);
protected void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args)
=> HandleMessageFlagsChangeAsync(args.UniqueId, args.Flags).ConfigureAwait(false);
protected async Task ManageUUIdBasedDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken = default)
{
var allUids = (await FolderService.GetKnownUidsForFolderAsync(localFolder.Id)).Select(a => new UniqueId(a)).ToList();
if (allUids.Count > 0)
protected async Task ManageUUIdBasedDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken = default)
{
var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken);
var deletedUids = allUids.Except(remoteAllUids).ToList();
var allUids = (await FolderService.GetKnownUidsForFolderAsync(localFolder.Id)).Select(a => new UniqueId(a)).ToList();
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
if (allUids.Count > 0)
{
var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken);
var deletedUids = allUids.Except(remoteAllUids).ToList();
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
}
}
}
}

View File

@@ -2,29 +2,30 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration;
namespace Wino.Core.Synchronizers.ImapSync;
internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider
namespace Wino.Core.Synchronizers.ImapSync
{
private readonly QResyncSynchronizer _qResyncSynchronizer;
private readonly CondstoreSynchronizer _condstoreSynchronizer;
private readonly UidBasedSynchronizer _uidBasedSynchronizer;
public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer)
internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider
{
_qResyncSynchronizer = qResyncSynchronizer;
_condstoreSynchronizer = condstoreSynchronizer;
_uidBasedSynchronizer = uidBasedSynchronizer;
}
private readonly QResyncSynchronizer _qResyncSynchronizer;
private readonly CondstoreSynchronizer _condstoreSynchronizer;
private readonly UidBasedSynchronizer _uidBasedSynchronizer;
public IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client)
{
if (client is not WinoImapClient winoImapClient)
throw new System.ArgumentException("Client must be of type WinoImapClient.", nameof(client));
public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer)
{
_qResyncSynchronizer = qResyncSynchronizer;
_condstoreSynchronizer = condstoreSynchronizer;
_uidBasedSynchronizer = uidBasedSynchronizer;
}
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer;
if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer;
public IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client)
{
if (client is not WinoImapClient winoImapClient)
throw new System.ArgumentException("Client must be of type WinoImapClient.", nameof(client));
return _uidBasedSynchronizer;
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer;
if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer;
return _uidBasedSynchronizer;
}
}
}

View File

@@ -12,109 +12,110 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Core.Synchronizers.ImapSync;
/// <summary>
/// RFC 5162 QRESYNC IMAP Synchronization strategy.
/// </summary>
internal class QResyncSynchronizer : ImapSynchronizationStrategyBase
namespace Wino.Core.Synchronizers.ImapSync
{
public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
/// <summary>
/// RFC 5162 QRESYNC IMAP Synchronization strategy.
/// </summary>
internal class QResyncSynchronizer : ImapSynchronizationStrategyBase
{
}
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client,
MailItemFolder folder,
IImapSynchronizer synchronizer,
CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();
if (client is not WinoImapClient winoClient)
throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient.");
if (!client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
throw new ImapSynchronizerStrategyException("Server does not support QRESYNC.");
if (!winoClient.IsQResyncEnabled)
throw new ImapSynchronizerStrategyException("QRESYNC is not enabled for WinoImapClient.");
// Ready to implement QRESYNC synchronization.
IMailFolder remoteFolder = null;
Folder = folder;
try
public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
{
remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
}
// Check the Uid validity first.
// If they don't match, clear all the local data and perform full-resync.
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client,
MailItemFolder folder,
IImapSynchronizer synchronizer,
CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();
bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity;
if (client is not WinoImapClient winoClient)
throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient.");
if (!isCacheValid)
if (!client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
throw new ImapSynchronizerStrategyException("Server does not support QRESYNC.");
if (!winoClient.IsQResyncEnabled)
throw new ImapSynchronizerStrategyException("QRESYNC is not enabled for WinoImapClient.");
// Ready to implement QRESYNC synchronization.
IMailFolder remoteFolder = null;
Folder = folder;
try
{
// TODO: Remove all local data.
}
remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
// Perform QRESYNC synchronization.
var localHighestModSeq = (ulong)folder.HighestModeSeq;
// Check the Uid validity first.
// If they don't match, clear all the local data and perform full-resync.
remoteFolder.MessagesVanished += OnMessagesVanished;
remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged;
bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity;
var allUids = await FolderService.GetKnownUidsForFolderAsync(folder.Id);
var allUniqueIds = allUids.Select(a => new UniqueId(a)).ToList();
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false);
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
// Update the local folder with the new highest mod-seq and validity.
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
folder.UidValidity = remoteFolder.UidValidity;
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
{
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder != null)
if (!isCacheValid)
{
remoteFolder.MessagesVanished -= OnMessagesVanished;
remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged;
// TODO: Remove all local data.
}
if (remoteFolder.IsOpen)
// Perform QRESYNC synchronization.
var localHighestModSeq = (ulong)folder.HighestModeSeq;
remoteFolder.MessagesVanished += OnMessagesVanished;
remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged;
var allUids = await FolderService.GetKnownUidsForFolderAsync(folder.Id);
var allUniqueIds = allUids.Select(a => new UniqueId(a)).ToList();
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false);
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
// Update the local folder with the new highest mod-seq and validity.
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
folder.UidValidity = remoteFolder.UidValidity;
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
{
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder != null)
{
await remoteFolder.CloseAsync();
remoteFolder.MessagesVanished -= OnMessagesVanished;
remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged;
if (remoteFolder.IsOpen)
{
await remoteFolder.CloseAsync();
}
}
}
}
return downloadedMessageIds;
}
return downloadedMessageIds;
}
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -10,71 +10,72 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration;
namespace Wino.Core.Synchronizers.ImapSync;
/// <summary>
/// Uid based IMAP Synchronization strategy.
/// </summary>
internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase
namespace Wino.Core.Synchronizers.ImapSync
{
public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService)
/// <summary>
/// Uid based IMAP Synchronization strategy.
/// </summary>
internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase
{
}
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
if (client is not WinoImapClient winoClient)
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
Folder = folder;
var downloadedMessageIds = new List<string>();
IMailFolder remoteFolder = null;
try
public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService)
{
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
// Fetch UIDs from the remote folder
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
remoteUids = remoteUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList();
await HandleChangedUIdsAsync(synchronizer, remoteFolder, remoteUids, cancellationToken).ConfigureAwait(false);
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
if (client is not WinoImapClient winoClient)
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
Folder = folder;
var downloadedMessageIds = new List<string>();
IMailFolder remoteFolder = null;
try
{
if (remoteFolder != null)
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
// Fetch UIDs from the remote folder
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
remoteUids = remoteUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList();
await HandleChangedUIdsAsync(synchronizer, remoteFolder, remoteUids, cancellationToken).ConfigureAwait(false);
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
}
catch (FolderNotFoundException)
{
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
return default;
}
catch (Exception)
{
throw;
}
finally
{
if (!cancellationToken.IsCancellationRequested)
{
if (remoteFolder.IsOpen)
if (remoteFolder != null)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
if (remoteFolder.IsOpen)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
}
return downloadedMessageIds;
}
return downloadedMessageIds;
}
internal override Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
internal override Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,434 +21,435 @@ using Wino.Core.Requests.Folder;
using Wino.Core.Requests.Mail;
using Wino.Messaging.UI;
namespace Wino.Core.Synchronizers;
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
namespace Wino.Core.Synchronizers
{
protected bool IsDisposing { get; private set; }
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
protected WinoSynchronizer(MailAccount account) : base(account) { }
/// <summary>
/// How many items per single HTTP call can be modified.
/// </summary>
public abstract uint BatchModificationSize { get; }
/// <summary>
/// How many items must be downloaded per folder when the folder is first synchronized.
/// </summary>
public abstract uint InitialMessageDownloadCountPerFolder { get; }
/// <summary>
/// Creates a new Wino Mail Item package out of native message type with full Mime.
/// </summary>
/// <param name="message">Native message type for the synchronizer.</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Package that encapsulates downloaded Mime and additional information for adding new mail.</returns>
public abstract Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes the aliases of the account.
/// Only available for Gmail right now.
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Internally synchronizes the account's mails with the given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Internally synchronizes the events of the account with given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public async Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
{
try
protected bool IsDisposing { get; private set; }
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
protected WinoSynchronizer(MailAccount account) : base(account) { }
/// <summary>
/// How many items per single HTTP call can be modified.
/// </summary>
public abstract uint BatchModificationSize { get; }
/// <summary>
/// How many items must be downloaded per folder when the folder is first synchronized.
/// </summary>
public abstract uint InitialMessageDownloadCountPerFolder { get; }
/// <summary>
/// Creates a new Wino Mail Item package out of native message type with full Mime.
/// </summary>
/// <param name="message">Native message type for the synchronizer.</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Package that encapsulates downloaded Mime and additional information for adding new mail.</returns>
public abstract Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes the aliases of the account.
/// Only available for Gmail right now.
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Internally synchronizes the account's mails with the given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Internally synchronizes the events of the account with given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public async Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
{
if (!ShouldQueueMailSynchronization(options))
try
{
Log.Debug($"{options.Type} synchronization is ignored.");
return MailSynchronizationResult.Canceled;
}
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
PublishSynchronizationProgress(1);
// ImapSynchronizer will send this type when an Idle client receives a notification of changes.
// We should not execute requests in this case.
bool shouldExecuteRequests = options.Type != MailSynchronizationType.IMAPIdle;
bool shouldDelayExecution = false;
int maxExecutionDelay = 0;
if (shouldExecuteRequests && changeRequestQueue.Any())
{
State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
List<IRequestBase> requestCopies = new(changeRequestQueue);
var keys = changeRequestQueue.GroupBy(a => a.GroupingKey());
foreach (var group in keys)
if (!ShouldQueueMailSynchronization(options))
{
var key = group.Key;
if (key is MailSynchronizerOperation mailSynchronizerOperation)
{
switch (mailSynchronizerOperation)
{
case MailSynchronizerOperation.MarkRead:
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
break;
case MailSynchronizerOperation.Move:
nativeRequests.AddRange(Move(new BatchMoveRequest(group.Cast<MoveRequest>())));
break;
case MailSynchronizerOperation.Delete:
nativeRequests.AddRange(Delete(new BatchDeleteRequest(group.Cast<DeleteRequest>())));
break;
case MailSynchronizerOperation.CreateDraft:
nativeRequests.AddRange(CreateDraft(group.ElementAt(0) as CreateDraftRequest));
break;
case MailSynchronizerOperation.Send:
nativeRequests.AddRange(SendDraft(group.ElementAt(0) as SendDraftRequest));
break;
case MailSynchronizerOperation.ChangeFlag:
nativeRequests.AddRange(ChangeFlag(new BatchChangeFlagRequest(group.Cast<ChangeFlagRequest>())));
break;
case MailSynchronizerOperation.AlwaysMoveTo:
nativeRequests.AddRange(AlwaysMoveTo(new BatchAlwaysMoveToRequest(group.Cast<AlwaysMoveToRequest>())));
break;
case MailSynchronizerOperation.MoveToFocused:
nativeRequests.AddRange(MoveToFocused(new BatchMoveToFocusedRequest(group.Cast<MoveToFocusedRequest>())));
break;
case MailSynchronizerOperation.Archive:
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
break;
default:
break;
}
}
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
{
switch (folderSynchronizerOperation)
{
case FolderSynchronizerOperation.RenameFolder:
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
break;
case FolderSynchronizerOperation.EmptyFolder:
nativeRequests.AddRange(EmptyFolder(group.ElementAt(0) as EmptyFolderRequest));
break;
case FolderSynchronizerOperation.MarkFolderRead:
nativeRequests.AddRange(MarkFolderAsRead(group.ElementAt(0) as MarkFolderAsReadRequest));
break;
default:
break;
}
}
Log.Debug($"{options.Type} synchronization is ignored.");
return MailSynchronizationResult.Canceled;
}
changeRequestQueue.Clear();
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
PublishUnreadItemChanges();
PublishSynchronizationProgress(1);
// Execute request sync options should be re-calculated after execution.
// This is the part we decide which individual folders must be synchronized
// after the batch request execution.
if (options.Type == MailSynchronizationType.ExecuteRequests)
options = GetSynchronizationOptionsAfterRequestExecution(requestCopies, options.Id);
// ImapSynchronizer will send this type when an Idle client receives a notification of changes.
// We should not execute requests in this case.
bool shouldExecuteRequests = options.Type != MailSynchronizationType.IMAPIdle;
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
// Bug: if Outlook can't create the message in Sent Items folder before this delay,
// message will not appear in user's inbox since it's not in the Sent Items folder.
bool shouldDelayExecution = false;
int maxExecutionDelay = 0;
shouldDelayExecution =
(Account.ProviderType == MailProviderType.Outlook)
&& requestCopies.Any(a => a.ResynchronizationDelay > 0);
if (shouldExecuteRequests && changeRequestQueue.Any())
{
State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
List<IRequestBase> requestCopies = new(changeRequestQueue);
var keys = changeRequestQueue.GroupBy(a => a.GroupingKey());
foreach (var group in keys)
{
var key = group.Key;
if (key is MailSynchronizerOperation mailSynchronizerOperation)
{
switch (mailSynchronizerOperation)
{
case MailSynchronizerOperation.MarkRead:
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
break;
case MailSynchronizerOperation.Move:
nativeRequests.AddRange(Move(new BatchMoveRequest(group.Cast<MoveRequest>())));
break;
case MailSynchronizerOperation.Delete:
nativeRequests.AddRange(Delete(new BatchDeleteRequest(group.Cast<DeleteRequest>())));
break;
case MailSynchronizerOperation.CreateDraft:
nativeRequests.AddRange(CreateDraft(group.ElementAt(0) as CreateDraftRequest));
break;
case MailSynchronizerOperation.Send:
nativeRequests.AddRange(SendDraft(group.ElementAt(0) as SendDraftRequest));
break;
case MailSynchronizerOperation.ChangeFlag:
nativeRequests.AddRange(ChangeFlag(new BatchChangeFlagRequest(group.Cast<ChangeFlagRequest>())));
break;
case MailSynchronizerOperation.AlwaysMoveTo:
nativeRequests.AddRange(AlwaysMoveTo(new BatchAlwaysMoveToRequest(group.Cast<AlwaysMoveToRequest>())));
break;
case MailSynchronizerOperation.MoveToFocused:
nativeRequests.AddRange(MoveToFocused(new BatchMoveToFocusedRequest(group.Cast<MoveToFocusedRequest>())));
break;
case MailSynchronizerOperation.Archive:
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
break;
default:
break;
}
}
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
{
switch (folderSynchronizerOperation)
{
case FolderSynchronizerOperation.RenameFolder:
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
break;
case FolderSynchronizerOperation.EmptyFolder:
nativeRequests.AddRange(EmptyFolder(group.ElementAt(0) as EmptyFolderRequest));
break;
case FolderSynchronizerOperation.MarkFolderRead:
nativeRequests.AddRange(MarkFolderAsRead(group.ElementAt(0) as MarkFolderAsReadRequest));
break;
default:
break;
}
}
}
changeRequestQueue.Clear();
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
PublishUnreadItemChanges();
// Execute request sync options should be re-calculated after execution.
// This is the part we decide which individual folders must be synchronized
// after the batch request execution.
if (options.Type == MailSynchronizationType.ExecuteRequests)
options = GetSynchronizationOptionsAfterRequestExecution(requestCopies, options.Id);
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
// Bug: if Outlook can't create the message in Sent Items folder before this delay,
// message will not appear in user's inbox since it's not in the Sent Items folder.
shouldDelayExecution =
(Account.ProviderType == MailProviderType.Outlook)
&& requestCopies.Any(a => a.ResynchronizationDelay > 0);
if (shouldDelayExecution)
{
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
}
// In terms of flag/read changes, there is no point of synchronizing must have folders.
options.ExcludeMustHaveFolders = requestCopies.All(a => a is ICustomFolderSynchronizationRequest request && request.ExcludeMustHaveFolders);
}
State = AccountSynchronizerState.Synchronizing;
// Handle special synchronization types.
// Profile information sync.
if (options.Type == MailSynchronizationType.UpdateProfile)
{
if (!Account.IsProfileInfoSyncSupported) return MailSynchronizationResult.Empty;
ProfileInformation newProfileInformation = null;
try
{
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
return MailSynchronizationResult.Failed;
}
return MailSynchronizationResult.Completed(newProfileInformation);
}
// Alias sync.
if (options.Type == MailSynchronizationType.Alias)
{
if (!Account.IsAliasSyncSupported) return MailSynchronizationResult.Empty;
try
{
await SynchronizeAliasesAsync();
return MailSynchronizationResult.Empty;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
return MailSynchronizationResult.Failed;
}
}
if (shouldDelayExecution)
{
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
await Task.Delay(maxExecutionDelay);
}
// In terms of flag/read changes, there is no point of synchronizing must have folders.
options.ExcludeMustHaveFolders = requestCopies.All(a => a is ICustomFolderSynchronizationRequest request && request.ExcludeMustHaveFolders);
// Start the internal synchronization.
var synchronizationResult = await SynchronizeMailsInternalAsync(options, activeSynchronizationCancellationToken).ConfigureAwait(false);
PublishUnreadItemChanges();
return synchronizationResult;
}
State = AccountSynchronizerState.Synchronizing;
// Handle special synchronization types.
// Profile information sync.
if (options.Type == MailSynchronizationType.UpdateProfile)
catch (OperationCanceledException)
{
if (!Account.IsProfileInfoSyncSupported) return MailSynchronizationResult.Empty;
Logger.Warning("Synchronization canceled.");
ProfileInformation newProfileInformation = null;
try
{
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
return MailSynchronizationResult.Failed;
}
return MailSynchronizationResult.Completed(newProfileInformation);
return MailSynchronizationResult.Canceled;
}
// Alias sync.
if (options.Type == MailSynchronizationType.Alias)
catch (Exception ex)
{
if (!Account.IsAliasSyncSupported) return MailSynchronizationResult.Empty;
Logger.Error(ex, "Synchronization failed for {Name}", Account.Name);
try
{
await SynchronizeAliasesAsync();
return MailSynchronizationResult.Empty;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
return MailSynchronizationResult.Failed;
}
throw;
}
if (shouldDelayExecution)
finally
{
await Task.Delay(maxExecutionDelay);
// Find the request and remove it from the pending list.
var pendingRequest = PendingSynchronizationRequest.FirstOrDefault(a => a.Key.Id == options.Id);
if (pendingRequest.Key != null)
{
PendingSynchronizationRequest.Remove(pendingRequest.Key);
}
// Reset account progress to hide the progress.
PublishSynchronizationProgress(0);
State = AccountSynchronizerState.Idle;
synchronizationSemaphore.Release();
}
// Start the internal synchronization.
var synchronizationResult = await SynchronizeMailsInternalAsync(options, activeSynchronizationCancellationToken).ConfigureAwait(false);
PublishUnreadItemChanges();
return synchronizationResult;
}
catch (OperationCanceledException)
{
Logger.Warning("Synchronization canceled.");
return MailSynchronizationResult.Canceled;
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
{
// TODO: Execute requests for calendar events.
return SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
}
catch (Exception ex)
/// <summary>
/// Updates unread item counts for some folders and account.
/// Sends a message that shell can pick up and update the UI.
/// </summary>
private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
/// <summary>
/// Sends a message to the shell to update the synchronization progress.
/// </summary>
/// <param name="progress">Percentage of the progress.</param>
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
/// <summary>
/// Attempts to find out the best possible synchronization options after the batch request execution.
/// </summary>
/// <param name="batches">Batch requests to run in synchronization.</param>
/// <returns>New synchronization options with minimal HTTP effort.</returns>
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
{
Logger.Error(ex, "Synchronization failed for {Name}", Account.Name);
List<Guid> synchronizationFolderIds = requests
.Where(a => a is ICustomFolderSynchronizationRequest)
.Cast<ICustomFolderSynchronizationRequest>()
.SelectMany(a => a.SynchronizationFolderIds)
.ToList();
throw;
}
finally
{
// Find the request and remove it from the pending list.
var pendingRequest = PendingSynchronizationRequest.FirstOrDefault(a => a.Key.Id == options.Id);
if (pendingRequest.Key != null)
var options = new MailSynchronizationOptions()
{
PendingSynchronizationRequest.Remove(pendingRequest.Key);
AccountId = Account.Id,
};
options.Id = existingSynchronizationId;
if (synchronizationFolderIds.Count > 0)
{
// Gather FolderIds to synchronize.
options.Type = MailSynchronizationType.CustomFolders;
options.SynchronizationFolderIds = synchronizationFolderIds;
}
else
{
// At this point it's a mix of everything. Do full sync.
options.Type = MailSynchronizationType.FullFolders;
}
// Reset account progress to hide the progress.
PublishSynchronizationProgress(0);
State = AccountSynchronizerState.Idle;
synchronizationSemaphore.Release();
}
}
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
{
// TODO: Execute requests for calendar events.
return SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
}
/// <summary>
/// Updates unread item counts for some folders and account.
/// Sends a message that shell can pick up and update the UI.
/// </summary>
private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
/// <summary>
/// Sends a message to the shell to update the synchronization progress.
/// </summary>
/// <param name="progress">Percentage of the progress.</param>
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
/// <summary>
/// Attempts to find out the best possible synchronization options after the batch request execution.
/// </summary>
/// <param name="batches">Batch requests to run in synchronization.</param>
/// <returns>New synchronization options with minimal HTTP effort.</returns>
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
{
List<Guid> synchronizationFolderIds = requests
.Where(a => a is ICustomFolderSynchronizationRequest)
.Cast<ICustomFolderSynchronizationRequest>()
.SelectMany(a => a.SynchronizationFolderIds)
.ToList();
var options = new MailSynchronizationOptions()
{
AccountId = Account.Id,
};
options.Id = existingSynchronizationId;
if (synchronizationFolderIds.Count > 0)
{
// Gather FolderIds to synchronize.
options.Type = MailSynchronizationType.CustomFolders;
options.SynchronizationFolderIds = synchronizationFolderIds;
}
else
{
// At this point it's a mix of everything. Do full sync.
options.Type = MailSynchronizationType.FullFolders;
return options;
}
return options;
}
/// <summary>
/// Checks if the mail synchronization should be queued or not.
/// </summary>
/// <param name="options">New mail sync request.</param>
/// <returns>Whether sync should be queued or not.</returns>
private bool ShouldQueueMailSynchronization(MailSynchronizationOptions options)
{
// Multiple IMAPIdle requests are ignored.
if (options.Type == MailSynchronizationType.IMAPIdle &&
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.IMAPIdle))
/// <summary>
/// Checks if the mail synchronization should be queued or not.
/// </summary>
/// <param name="options">New mail sync request.</param>
/// <returns>Whether sync should be queued or not.</returns>
private bool ShouldQueueMailSynchronization(MailSynchronizationOptions options)
{
return false;
// Multiple IMAPIdle requests are ignored.
if (options.Type == MailSynchronizationType.IMAPIdle &&
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.IMAPIdle))
{
return false;
}
// Executing requests may trigger idle sync.
// If there are pending execute requests cancel idle change.
// TODO: Ideally this check should only work for Inbox execute requests.
// Check if request folders contains Inbox.
if (options.Type == MailSynchronizationType.IMAPIdle &&
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.ExecuteRequests))
{
return false;
}
return true;
}
// Executing requests may trigger idle sync.
// If there are pending execute requests cancel idle change.
#region Mail/Folder Operations
// TODO: Ideally this check should only work for Inbox execute requests.
// Check if request folders contains Inbox.
public virtual bool DelaySendOperationSynchronization() => false;
public virtual List<IRequestBundle<TBaseRequest>> Move(BatchMoveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> ChangeFlag(BatchChangeFlagRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MarkRead(BatchMarkReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> Delete(BatchDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> AlwaysMoveTo(BatchAlwaysMoveToRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> CreateDraft(CreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> SendDraft(SendDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
if (options.Type == MailSynchronizationType.IMAPIdle &&
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.ExecuteRequests))
#endregion
#region Calendar Operations
#endregion
/// <summary>
/// Downloads a single missing message from synchronizer and saves it to given FileId from IMailItem.
/// </summary>
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public List<IRequestBundle<ImapRequest>> CreateSingleTaskBundle(Func<IImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
{
return false;
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
}
return true;
}
#region Mail/Folder Operations
public virtual bool DelaySendOperationSynchronization() => false;
public virtual List<IRequestBundle<TBaseRequest>> Move(BatchMoveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> ChangeFlag(BatchChangeFlagRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MarkRead(BatchMarkReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> Delete(BatchDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> AlwaysMoveTo(BatchAlwaysMoveToRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> CreateDraft(CreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> SendDraft(SendDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
#endregion
#region Calendar Operations
#endregion
/// <summary>
/// Downloads a single missing message from synchronizer and saves it to given FileId from IMailItem.
/// </summary>
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public List<IRequestBundle<ImapRequest>> CreateSingleTaskBundle(Func<IImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
{
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
}
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<IImapClient, TSingeRequestType, Task> value,
List<TSingeRequestType> requests)
where TSingeRequestType : IRequestBase, IUIChangeRequest
{
List<IRequestBundle<ImapRequest>> ret = [];
foreach (var request in requests)
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<IImapClient, TSingeRequestType, Task> value,
List<TSingeRequestType> requests)
where TSingeRequestType : IRequestBase, IUIChangeRequest
{
ret.Add(new ImapRequestBundle(new ImapRequest<TSingeRequestType>(value, request), request, request));
List<IRequestBundle<ImapRequest>> ret = [];
foreach (var request in requests)
{
ret.Add(new ImapRequestBundle(new ImapRequest<TSingeRequestType>(value, request), request, request));
}
return ret;
}
return ret;
}
public virtual Task KillSynchronizerAsync()
{
IsDisposing = true;
CancelAllSynchronizations();
return Task.CompletedTask;
}
protected void CancelAllSynchronizations()
{
foreach (var request in PendingSynchronizationRequest)
public virtual Task KillSynchronizerAsync()
{
request.Value.Cancel();
request.Value.Dispose();
IsDisposing = true;
CancelAllSynchronizations();
return Task.CompletedTask;
}
protected void CancelAllSynchronizations()
{
foreach (var request in PendingSynchronizationRequest)
{
request.Value.Cancel();
request.Value.Dispose();
}
}
}
}