@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user