IMAP Improvements (#558)
* Fixing an issue where scrollviewer overrides a part of template in mail list. Adjusted zoomed out header grid's corner radius. * IDLE implementation, imap synchronization strategies basics and condstore synchronization. * Adding iCloud and Yahoo as special IMAP handling scenario. * iCloud special imap handling. * Support for killing synchronizers. * Update privacy policy url. * Batching condstore downloads into 50, using SORT extension for searches if supported. * Bumping some nugets. More on the imap synchronizers. * Delegating idle synchronizations to server to post-sync operations. * Update mailkit to resolve qresync bug with iCloud. * Fixing remote highest mode seq checks for qresync and condstore synchronizers. * Yahoo custom settings. * Bump google sdk package. * Fixing the build issue.... * NRE on canceled token accounts during setup. * Server crash handlers. * Remove ARM32. Upgrade server to .NET 9. * Fix icons for yahoo and apple. * Fixed an issue where disabled folders causing an exception on forced sync. * Remove smtp encoding constraint. * Remove commented code. * Fixing merge conflict * Addressing double registrations for mailkit remote folder events in synchronizers. * Making sure idle canceled result is not reported. * Fixing custom imap server dialog opening. * Fixing the issue with account creation making the previously selected account as selected as well. * Fixing app close behavior and logging app close.
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace Wino.Authentication
|
|
||||||
{
|
|
||||||
public class Office365Authenticator : OutlookAuthenticator
|
|
||||||
{
|
|
||||||
public Office365Authenticator(INativeAppService nativeAppService,
|
|
||||||
IApplicationConfiguration applicationConfiguration,
|
|
||||||
IAuthenticatorConfig authenticatorConfig) : base(nativeAppService, applicationConfiguration, authenticatorConfig)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MailProviderType ProviderType => MailProviderType.Office365;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,14 +20,6 @@
|
|||||||
<Configuration>Release</Configuration>
|
<Configuration>Release</Configuration>
|
||||||
<Platform>x64</Platform>
|
<Platform>x64</Platform>
|
||||||
</ProjectConfiguration>
|
</ProjectConfiguration>
|
||||||
<ProjectConfiguration Include="Debug|ARM">
|
|
||||||
<Configuration>Debug</Configuration>
|
|
||||||
<Platform>ARM</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
<ProjectConfiguration Include="Release|ARM">
|
|
||||||
<Configuration>Release</Configuration>
|
|
||||||
<Platform>ARM</Platform>
|
|
||||||
</ProjectConfiguration>
|
|
||||||
<ProjectConfiguration Include="Debug|ARM64">
|
<ProjectConfiguration Include="Debug|ARM64">
|
||||||
<Configuration>Debug</Configuration>
|
<Configuration>Debug</Configuration>
|
||||||
<Platform>ARM64</Platform>
|
<Platform>ARM64</Platform>
|
||||||
@@ -76,7 +68,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
|
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
|
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ namespace Wino.Calendar.ViewModels
|
|||||||
if (accountCreationDialogResult == null) return;
|
if (accountCreationDialogResult == null) return;
|
||||||
|
|
||||||
var accountCreationCancellationTokenSource = new CancellationTokenSource();
|
var accountCreationCancellationTokenSource = new CancellationTokenSource();
|
||||||
var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
|
var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult);
|
||||||
|
|
||||||
accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource);
|
accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource);
|
||||||
accountCreationDialog.State = AccountCreationDialogState.SigningIn;
|
accountCreationDialog.State = AccountCreationDialogState.SigningIn;
|
||||||
@@ -92,7 +92,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
{
|
{
|
||||||
ProviderType = accountCreationDialogResult.ProviderType,
|
ProviderType = accountCreationDialogResult.ProviderType,
|
||||||
Name = accountCreationDialogResult.AccountName,
|
Name = accountCreationDialogResult.AccountName,
|
||||||
AccountColorHex = accountCreationDialogResult.AccountColorHex,
|
|
||||||
Id = Guid.NewGuid()
|
Id = Guid.NewGuid()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,13 +103,8 @@ namespace Wino.Calendar.ViewModels
|
|||||||
if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
|
if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
|
||||||
throw new AccountSetupCanceledException();
|
throw new AccountSetupCanceledException();
|
||||||
|
|
||||||
|
|
||||||
tokenInformationResponse.ThrowIfFailed();
|
tokenInformationResponse.ThrowIfFailed();
|
||||||
|
|
||||||
//var tokenInformation = tokenInformationResponse.Data;
|
|
||||||
//createdAccount.Address = tokenInformation.Address;
|
|
||||||
//tokenInformation.AccountId = createdAccount.Id;
|
|
||||||
|
|
||||||
await AccountService.CreateAccountAsync(createdAccount, null);
|
await AccountService.CreateAccountAsync(createdAccount, null);
|
||||||
|
|
||||||
// Sync profile information if supported.
|
// Sync profile information if supported.
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
|
|
||||||
<PackageReference Include="TimePeriodLibrary.NET" Version="2.1.5" />
|
<PackageReference Include="TimePeriodLibrary.NET" Version="2.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Wino.Calendar.Services
|
|||||||
|
|
||||||
foreach (var type in providers)
|
foreach (var type in providers)
|
||||||
{
|
{
|
||||||
providerList.Add(new ProviderDetail(type));
|
providerList.Add(new ProviderDetail(type, SpecialImapProvider.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
return providerList;
|
return providerList;
|
||||||
|
|||||||
@@ -60,29 +60,6 @@
|
|||||||
<Prefer32Bit>true</Prefer32Bit>
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<OutputPath>bin\ARM\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
|
||||||
<NoWarn>;2008</NoWarn>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<PlatformTarget>ARM</PlatformTarget>
|
|
||||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<Prefer32Bit>true</Prefer32Bit>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
|
|
||||||
<OutputPath>bin\ARM\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<NoWarn>;2008</NoWarn>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<PlatformTarget>ARM</PlatformTarget>
|
|
||||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<Prefer32Bit>true</Prefer32Bit>
|
|
||||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<OutputPath>bin\ARM64\Debug\</OutputPath>
|
<OutputPath>bin\ARM64\Debug\</OutputPath>
|
||||||
@@ -334,7 +311,7 @@
|
|||||||
<Version>6.2.14</Version>
|
<Version>6.2.14</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Win2D.uwp">
|
<PackageReference Include="Win2D.uwp">
|
||||||
<Version>1.28.0</Version>
|
<Version>1.28.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ namespace Wino.Core.Domain.Entities.Shared
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? MergedInboxId { get; set; }
|
public Guid? MergedInboxId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the additional IMAP provider assignment for the account.
|
||||||
|
/// Providers that use IMAP as a synchronizer but have special requirements.
|
||||||
|
/// </summary>
|
||||||
|
public SpecialImapProvider SpecialImapProvider { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the merged inbox this account belongs to.
|
/// Contains the merged inbox this account belongs to.
|
||||||
/// Ignored for all SQLite operations.
|
/// Ignored for all SQLite operations.
|
||||||
@@ -95,7 +101,7 @@ namespace Wino.Core.Domain.Entities.Shared
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform ProfileInformation sync type.
|
/// Gets whether the account can perform ProfileInformation sync type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail;
|
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Gmail;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform AliasInformation sync type.
|
/// Gets whether the account can perform AliasInformation sync type.
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
{
|
{
|
||||||
Outlook,
|
Outlook,
|
||||||
Gmail,
|
Gmail,
|
||||||
Office365,
|
IMAP4 = 4 // 2-3 were removed after release. Don't change for backward compatibility.
|
||||||
Yahoo,
|
|
||||||
IMAP4
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
{
|
{
|
||||||
public enum MailSynchronizationType
|
public enum MailSynchronizationType
|
||||||
{
|
{
|
||||||
// Shared
|
|
||||||
UpdateProfile, // Only update profile information
|
UpdateProfile, // Only update profile information
|
||||||
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
||||||
FoldersOnly, // Only synchronize folder metadata.
|
FoldersOnly, // Only synchronize folder metadata.
|
||||||
InboxOnly, // Only Inbox, Sent and Draft folders.
|
InboxOnly, // Only Inbox, Sent, Draft and Deleted folders.
|
||||||
CustomFolders, // Only sync folders that are specified in the options.
|
CustomFolders, // Only sync folders that are specified in the options.
|
||||||
FullFolders, // Synchronize all folders. This won't update profile or alias information.
|
FullFolders, // Synchronize all folders. This won't update profile or alias information.
|
||||||
Alias, // Only update alias information
|
Alias, // Only update alias information
|
||||||
|
IMAPIdle // Idle client triggered synchronization.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
Wino.Core.Domain/Enums/SpecialImapProvider.cs
Normal file
9
Wino.Core.Domain/Enums/SpecialImapProvider.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums
|
||||||
|
{
|
||||||
|
public enum SpecialImapProvider
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
iCloud,
|
||||||
|
Yahoo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Exceptions
|
||||||
|
{
|
||||||
|
public class ImapSynchronizerStrategyException : System.Exception
|
||||||
|
{
|
||||||
|
public ImapSynchronizerStrategyException(string message) : base(message)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces
|
namespace Wino.Core.Domain.Interfaces
|
||||||
{
|
{
|
||||||
public interface IAccountCreationDialog
|
public interface IAccountCreationDialog
|
||||||
{
|
{
|
||||||
void ShowDialog(CancellationTokenSource cancellationTokenSource);
|
Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource);
|
||||||
void Complete(bool cancel);
|
void Complete(bool cancel);
|
||||||
AccountCreationDialogState State { get; set; }
|
AccountCreationDialogState State { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// <param name="request">Request to queue.</param>
|
/// <param name="request">Request to queue.</param>
|
||||||
void QueueRequest(IRequestBase request);
|
void QueueRequest(IRequestBase request);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// TODO
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Whether active synchronization is stopped or not.</returns>
|
|
||||||
bool CancelActiveSynchronization();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Synchronizes profile information with the server.
|
/// Synchronizes profile information with the server.
|
||||||
/// Sender name and Profile picture are updated.
|
/// Sender name and Profile picture are updated.
|
||||||
|
|||||||
@@ -13,5 +13,10 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// Which folders to sync after this operation?
|
/// Which folders to sync after this operation?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
List<Guid> SynchronizationFolderIds { get; }
|
List<Guid> SynchronizationFolderIds { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, additional folders like Sent, Drafts and Deleted will not be synchronized
|
||||||
|
/// </summary>
|
||||||
|
bool ExcludeMustHaveFolders { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
string dontAskAgainConfigurationKey = "");
|
string dontAskAgainConfigurationKey = "");
|
||||||
Task<bool> ShowCustomThemeBuilderDialogAsync();
|
Task<bool> ShowCustomThemeBuilderDialogAsync();
|
||||||
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
||||||
IAccountCreationDialog GetAccountCreationDialog(MailProviderType type);
|
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
||||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Wino.Core.Domain.Entities.Shared;
|
|||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces
|
namespace Wino.Core.Domain.Interfaces
|
||||||
{
|
{
|
||||||
public interface ICustomServerAccountCreationDialog : IAccountCreationDialog
|
public interface IImapAccountCreationDialog : IAccountCreationDialog
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the custom server information from the dialog..
|
/// Returns the custom server information from the dialog..
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using MailKit.Net.Imap;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a synchronization strategy for synchronizing IMAP folders based on the server capabilities.
|
||||||
|
/// </summary>
|
||||||
|
public interface IImapSynchronizationStrategyProvider
|
||||||
|
{
|
||||||
|
IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Wino.Core.Domain/Interfaces/IImapSynchronizer.cs
Normal file
17
Wino.Core.Domain/Interfaces/IImapSynchronizer.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
public interface IImapSynchronizer
|
||||||
|
{
|
||||||
|
uint InitialMessageDownloadCountPerFolder { get; }
|
||||||
|
|
||||||
|
Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
|
||||||
|
Task StartIdleClientAsync();
|
||||||
|
Task StopIdleClientAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
Normal file
22
Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
public interface IImapSynchronizerStrategy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronizes given folder with the ImapClient client from the client pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to perform sync with. I love Mira and Jasminka</param>
|
||||||
|
/// <param name="folder">Folder to synchronize.</param>
|
||||||
|
/// <param name="synchronizer">Imap synchronizer that downloads messages.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>List of new downloaded message ids that don't exist locally.</returns>
|
||||||
|
Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MailKit;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
@@ -108,5 +109,13 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
|
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
|
||||||
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
|
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
|
||||||
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
|
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns ids
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId"></param>
|
||||||
|
/// <param name="uniqueIds"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<MailCopy>> GetExistingMailsAsync(Guid folderId, IEnumerable<UniqueId> uniqueIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
public interface IProviderDetail
|
public interface IProviderDetail
|
||||||
{
|
{
|
||||||
MailProviderType Type { get; }
|
MailProviderType Type { get; }
|
||||||
|
SpecialImapProvider SpecialImapProvider { get; }
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
string Description { get; }
|
string Description { get; }
|
||||||
string ProviderImage { get; }
|
string ProviderImage { get; }
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISpecialImapProviderConfigResolver
|
||||||
|
{
|
||||||
|
CustomServerInformation GetServerInformation(MailAccount account, AccountCreationDialogResult dialogResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,6 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
{
|
{
|
||||||
Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId);
|
Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId);
|
||||||
Task InitializeAsync();
|
Task InitializeAsync();
|
||||||
|
Task DeleteSynchronizerAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,12 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
|
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 1. Cancel active synchronization.
|
||||||
|
/// 2. Stop all running tasks.
|
||||||
|
/// 3. Dispose all resources.
|
||||||
|
/// </summary>
|
||||||
|
Task KillSynchronizerAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts
|
namespace Wino.Core.Domain.Models.Accounts
|
||||||
{
|
{
|
||||||
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string AccountColorHex = "");
|
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, SpecialImapProviderDetails SpecialImapProviderDetails);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,32 @@ namespace Wino.Core.Domain.Models.Accounts
|
|||||||
public class ProviderDetail : IProviderDetail
|
public class ProviderDetail : IProviderDetail
|
||||||
{
|
{
|
||||||
public MailProviderType Type { get; }
|
public MailProviderType Type { get; }
|
||||||
|
public SpecialImapProvider SpecialImapProvider { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
|
|
||||||
public string ProviderImage => $"/Wino.Core.UWP/Assets/Providers/{Type}.png";
|
public string ProviderImage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (SpecialImapProvider == SpecialImapProvider.None)
|
||||||
|
{
|
||||||
|
return $"/Wino.Core.UWP/Assets/Providers/{Type}.png";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $"/Wino.Core.UWP/Assets/Providers/{SpecialImapProvider}.png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
|
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
|
||||||
|
|
||||||
public ProviderDetail(MailProviderType type)
|
public ProviderDetail(MailProviderType type, SpecialImapProvider specialImapProvider)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
|
SpecialImapProvider = specialImapProvider;
|
||||||
|
|
||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
@@ -25,21 +39,29 @@ namespace Wino.Core.Domain.Models.Accounts
|
|||||||
Name = "Outlook";
|
Name = "Outlook";
|
||||||
Description = "Outlook.com, Live.com, Hotmail, MSN";
|
Description = "Outlook.com, Live.com, Hotmail, MSN";
|
||||||
break;
|
break;
|
||||||
case MailProviderType.Office365:
|
|
||||||
Name = "Office 365";
|
|
||||||
Description = "Office 365, Exchange";
|
|
||||||
break;
|
|
||||||
case MailProviderType.Gmail:
|
case MailProviderType.Gmail:
|
||||||
Name = "Gmail";
|
Name = "Gmail";
|
||||||
Description = Translator.ProviderDetail_Gmail_Description;
|
Description = Translator.ProviderDetail_Gmail_Description;
|
||||||
break;
|
break;
|
||||||
case MailProviderType.Yahoo:
|
|
||||||
Name = "Yahoo";
|
|
||||||
Description = "Yahoo Mail";
|
|
||||||
break;
|
|
||||||
case MailProviderType.IMAP4:
|
case MailProviderType.IMAP4:
|
||||||
Name = Translator.ProviderDetail_IMAP_Title;
|
switch (specialImapProvider)
|
||||||
Description = Translator.ProviderDetail_IMAP_Description;
|
{
|
||||||
|
case SpecialImapProvider.None:
|
||||||
|
Name = Translator.ProviderDetail_IMAP_Title;
|
||||||
|
Description = Translator.ProviderDetail_IMAP_Description;
|
||||||
|
break;
|
||||||
|
case SpecialImapProvider.iCloud:
|
||||||
|
Name = Translator.ProviderDetail_iCloud_Title;
|
||||||
|
Description = Translator.ProviderDetail_iCloud_Description;
|
||||||
|
break;
|
||||||
|
case SpecialImapProvider.Yahoo:
|
||||||
|
Name = Translator.ProviderDetail_Yahoo_Title;
|
||||||
|
Description = Translator.ProviderDetail_Yahoo_Description;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Models.Accounts
|
||||||
|
{
|
||||||
|
public record SpecialImapProviderDetails(string Address, string Password, string SenderName, SpecialImapProvider SpecialImapProvider);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MailKit;
|
||||||
|
using MimeKit;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Models.MailItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encapsulates all required information to create a MimeMessage for IMAP synchronizer.
|
||||||
|
/// </summary>
|
||||||
|
public class ImapMessageCreationPackage
|
||||||
|
{
|
||||||
|
public IMessageSummary MessageSummary { get; }
|
||||||
|
public MimeMessage MimeMessage { get; }
|
||||||
|
|
||||||
|
public ImapMessageCreationPackage(IMessageSummary messageSummary, MimeMessage mimeMessage)
|
||||||
|
{
|
||||||
|
MessageSummary = messageSummary;
|
||||||
|
MimeMessage = mimeMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace Wino.Core.Domain.Models.Synchronization
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique id of synchronization.
|
/// Unique id of synchronization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid Id { get; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Account to execute synchronization for.
|
/// Account to execute synchronization for.
|
||||||
@@ -26,6 +26,12 @@ namespace Wino.Core.Domain.Models.Synchronization
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Guid> SynchronizationFolderIds { get; set; }
|
public List<Guid> SynchronizationFolderIds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, additional folders like Sent,Drafts and Deleted will not be synchronized
|
||||||
|
/// with InboxOnly and CustomFolders sync type.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExcludeMustHaveFolders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When doing a linked inbox synchronization, we must ignore reporting completion to the caller for each folder.
|
/// When doing a linked inbox synchronization, we must ignore reporting completion to the caller for each folder.
|
||||||
/// This Id will help tracking that. Id is unique, but this one can be the same for all sync requests
|
/// This Id will help tracking that. Id is unique, but this one can be the same for all sync requests
|
||||||
@@ -33,6 +39,6 @@ namespace Wino.Core.Domain.Models.Synchronization
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? GroupedSynchronizationTrackingId { get; set; }
|
public Guid? GroupedSynchronizationTrackingId { get; set; }
|
||||||
|
|
||||||
public override string ToString() => $"Type: {Type}, Folders: {(SynchronizationFolderIds == null ? "All" : string.Join(",", SynchronizationFolderIds))}";
|
public override string ToString() => $"Type: {Type}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,10 @@
|
|||||||
"ProviderDetail_Gmail_Description": "Google Account",
|
"ProviderDetail_Gmail_Description": "Google Account",
|
||||||
"ProviderDetail_IMAP_Description": "Custom IMAP/SMTP server",
|
"ProviderDetail_IMAP_Description": "Custom IMAP/SMTP server",
|
||||||
"ProviderDetail_IMAP_Title": "IMAP Server",
|
"ProviderDetail_IMAP_Title": "IMAP Server",
|
||||||
|
"ProviderDetail_Yahoo_Title": "Yahoo Mail",
|
||||||
|
"ProviderDetail_Yahoo_Description": "Yahoo Account",
|
||||||
|
"ProviderDetail_iCloud_Title": "iCloud",
|
||||||
|
"ProviderDetail_iCloud_Description": "Apple iCloud Account",
|
||||||
"ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.",
|
"ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.",
|
||||||
"Results": "Results",
|
"Results": "Results",
|
||||||
"Right": "Right",
|
"Right": "Right",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
Wino.Core.UWP/Assets/Providers/iCloud.png
Normal file
BIN
Wino.Core.UWP/Assets/Providers/iCloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
132
Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml
Normal file
132
Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml
Normal file
File diff suppressed because one or more lines are too long
76
Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml.cs
Normal file
76
Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Wino.Core.UWP.Controls
|
||||||
|
{
|
||||||
|
public sealed partial class AccountCreationDialogControl : UserControl, IRecipient<CopyAuthURLRequested>
|
||||||
|
{
|
||||||
|
private string copyClipboardURL;
|
||||||
|
|
||||||
|
public event EventHandler CancelClicked;
|
||||||
|
|
||||||
|
public AccountCreationDialogState State
|
||||||
|
{
|
||||||
|
get { return (AccountCreationDialogState)GetValue(StateProperty); }
|
||||||
|
set { SetValue(StateProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(AccountCreationDialogState), typeof(AccountCreationDialogControl), new PropertyMetadata(AccountCreationDialogState.Idle, new PropertyChangedCallback(OnStateChanged)));
|
||||||
|
|
||||||
|
public AccountCreationDialogControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnStateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (obj is AccountCreationDialogControl dialog)
|
||||||
|
{
|
||||||
|
dialog.UpdateVisualStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisualStates() => VisualStateManager.GoToState(this, State.ToString(), false);
|
||||||
|
|
||||||
|
public async void Receive(CopyAuthURLRequested message)
|
||||||
|
{
|
||||||
|
copyClipboardURL = message.AuthURL;
|
||||||
|
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
|
||||||
|
{
|
||||||
|
AuthHelpDialogButton.Visibility = Windows.UI.Xaml.Visibility.Visible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ControlLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ControlUnloaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CopyClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(copyClipboardURL)) return;
|
||||||
|
|
||||||
|
var clipboardService = WinoApplication.Current.Services.GetService<IClipboardService>();
|
||||||
|
await clipboardService.CopyClipboardAsync(copyClipboardURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CancelButtonClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) => CancelClicked?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,9 @@ namespace Wino.Core.UWP.Controls
|
|||||||
{ WinoIconGlyph.EventRespond, "\uE924" },
|
{ WinoIconGlyph.EventRespond, "\uE924" },
|
||||||
{ WinoIconGlyph.EventReminder, "\uE923" },
|
{ WinoIconGlyph.EventReminder, "\uE923" },
|
||||||
{ WinoIconGlyph.EventJoinOnline, "\uE926" },
|
{ WinoIconGlyph.EventJoinOnline, "\uE926" },
|
||||||
{ WinoIconGlyph.ViewMessageSource, "\uE943" }
|
{ WinoIconGlyph.ViewMessageSource, "\uE943" },
|
||||||
|
{ WinoIconGlyph.Apple, "\uE92B" },
|
||||||
|
{ WinoIconGlyph.Yahoo, "\uE92C" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ namespace Wino.Core.UWP.Controls
|
|||||||
EventEditSeries,
|
EventEditSeries,
|
||||||
EventJoinOnline,
|
EventJoinOnline,
|
||||||
ViewMessageSource,
|
ViewMessageSource,
|
||||||
|
Apple,
|
||||||
|
Yahoo
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class WinoFontIcon : FontIcon
|
public partial class WinoFontIcon : FontIcon
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,50 +1,71 @@
|
|||||||
using System;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Windows.UI.Xaml;
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.UWP;
|
|
||||||
using Wino.Messaging.UI;
|
|
||||||
|
|
||||||
namespace Wino.Dialogs
|
namespace Wino.Dialogs
|
||||||
{
|
{
|
||||||
public sealed partial class AccountCreationDialog : BaseAccountCreationDialog, IRecipient<CopyAuthURLRequested>
|
public sealed partial class AccountCreationDialog : ContentDialog, IAccountCreationDialog
|
||||||
{
|
{
|
||||||
private string copyClipboardURL;
|
private TaskCompletionSource<bool> dialogOpened = new TaskCompletionSource<bool>();
|
||||||
|
public CancellationTokenSource CancellationTokenSource { get; private set; }
|
||||||
|
|
||||||
|
public AccountCreationDialogState State
|
||||||
|
{
|
||||||
|
get { return (AccountCreationDialogState)GetValue(StateProperty); }
|
||||||
|
set { SetValue(StateProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(AccountCreationDialogState), typeof(AccountCreationDialog), new PropertyMetadata(AccountCreationDialogState.Idle));
|
||||||
|
|
||||||
public AccountCreationDialog()
|
public AccountCreationDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Register(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnStateChanged(AccountCreationDialogState state)
|
// Prevent users from dismissing it by ESC key.
|
||||||
|
public void DialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
||||||
{
|
{
|
||||||
var tt = VisualStateManager.GoToState(this, state.ToString(), true);
|
if (args.Result == ContentDialogResult.None)
|
||||||
}
|
|
||||||
|
|
||||||
public async void Receive(CopyAuthURLRequested message)
|
|
||||||
{
|
|
||||||
copyClipboardURL = message.AuthURL;
|
|
||||||
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
|
|
||||||
{
|
{
|
||||||
AuthHelpDialogButton.Visibility = Windows.UI.Xaml.Visibility.Visible;
|
args.Cancel = true;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) => Complete(true);
|
public void Complete(bool cancel)
|
||||||
|
|
||||||
private async void CopyClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(copyClipboardURL)) return;
|
State = cancel ? AccountCreationDialogState.Canceled : AccountCreationDialogState.Completed;
|
||||||
|
|
||||||
var clipboardService = WinoApplication.Current.Services.GetService<IClipboardService>();
|
// Unregister from closing event.
|
||||||
await clipboardService.CopyClipboardAsync(copyClipboardURL);
|
Closing -= DialogClosing;
|
||||||
|
|
||||||
|
if (cancel && !CancellationTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
CancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelClicked(object sender, System.EventArgs e) => Complete(true);
|
||||||
|
|
||||||
|
public async Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource)
|
||||||
|
{
|
||||||
|
CancellationTokenSource = cancellationTokenSource;
|
||||||
|
|
||||||
|
Opened += DialogOpened;
|
||||||
|
_ = ShowAsync();
|
||||||
|
|
||||||
|
await dialogOpened.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args)
|
||||||
|
{
|
||||||
|
Opened -= DialogOpened;
|
||||||
|
|
||||||
|
dialogOpened?.SetResult(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using Windows.UI.Xaml;
|
|
||||||
using Windows.UI.Xaml.Controls;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace Wino.Dialogs
|
|
||||||
{
|
|
||||||
public abstract class BaseAccountCreationDialog : ContentDialog, IAccountCreationDialog
|
|
||||||
{
|
|
||||||
public AccountCreationDialogState State
|
|
||||||
{
|
|
||||||
get { return (AccountCreationDialogState)GetValue(StateProperty); }
|
|
||||||
set { SetValue(StateProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public CancellationTokenSource CancellationTokenSource { get; private set; }
|
|
||||||
|
|
||||||
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(AccountCreationDialogState), typeof(BaseAccountCreationDialog), new PropertyMetadata(AccountCreationDialogState.Idle, OnStateChanged));
|
|
||||||
|
|
||||||
private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
var dialog = d as BaseAccountCreationDialog;
|
|
||||||
dialog.OnStateChanged((AccountCreationDialogState)e.NewValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void OnStateChanged(AccountCreationDialogState state);
|
|
||||||
|
|
||||||
// Prevent users from dismissing it by ESC key.
|
|
||||||
public void DialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.Result == ContentDialogResult.None)
|
|
||||||
{
|
|
||||||
args.Cancel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowDialog(CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
CancellationTokenSource = cancellationTokenSource;
|
|
||||||
|
|
||||||
_ = ShowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Complete(bool cancel)
|
|
||||||
{
|
|
||||||
State = cancel ? AccountCreationDialogState.Canceled : AccountCreationDialogState.Completed;
|
|
||||||
|
|
||||||
// Unregister from closing event.
|
|
||||||
Closing -= DialogClosing;
|
|
||||||
|
|
||||||
if (cancel && !CancellationTokenSource.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
CancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
Hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,38 +43,91 @@
|
|||||||
</ContentDialog.Resources>
|
</ContentDialog.Resources>
|
||||||
|
|
||||||
<Grid MinWidth="400" RowSpacing="12">
|
<Grid MinWidth="400" RowSpacing="12">
|
||||||
<Grid.RowDefinitions>
|
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
|
||||||
<RowDefinition Height="Auto" />
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Account Name -->
|
<!-- Account Name -->
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="AccountNameTextbox"
|
x:Name="AccountNameTextbox"
|
||||||
Header="{x:Bind domain:Translator.NewAccountDialog_AccountName}"
|
Header="{x:Bind domain:Translator.NewAccountDialog_AccountName}"
|
||||||
PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}"
|
PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}"
|
||||||
TextChanged="AccountNameChanged" />
|
TextChanged="InputChanged" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
TODO: Move Name, Sender Name and Color Picker to another Frame.
|
TODO: Move Name, Sender Name and Color Picker to another Frame.
|
||||||
Provider selection should be first, then account details.
|
Provider selection should be first, then account details.
|
||||||
-->
|
-->
|
||||||
<!--<Grid Grid.Row="1">
|
<!--<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Text="Color" />
|
<TextBlock Text="Color" />
|
||||||
<muxc:ColorPicker x:Name="AccountColorPicker" Grid.Column="1" />
|
<muxc:ColorPicker x:Name="AccountColorPicker" Grid.Column="1" />
|
||||||
</Grid>-->
|
</Grid>-->
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
ItemTemplate="{StaticResource NewMailProviderTemplate}"
|
ItemTemplate="{StaticResource NewMailProviderTemplate}"
|
||||||
ItemsSource="{x:Bind Providers}"
|
ItemsSource="{x:Bind Providers}"
|
||||||
SelectedItem="{x:Bind SelectedMailProvider, Mode=TwoWay}"
|
SelectedItem="{x:Bind SelectedMailProvider, Mode=TwoWay}"
|
||||||
SelectionMode="Single" />
|
SelectionMode="Single" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Known special IMAP login details. -->
|
||||||
|
<Grid RowSpacing="12" Visibility="{x:Bind IsSpecialImapServerPartVisible, Mode=OneWay}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Button Click="BackClicked">
|
||||||
|
<Button.Content>
|
||||||
|
<Viewbox Width="16">
|
||||||
|
<PathIcon Data="F1 M 20 9.375 C 20 9.544271 19.93815 9.690756 19.814453 9.814453 C 19.690754 9.938151 19.54427 10 19.375 10 L 2.138672 10 L 9.814453 17.685547 C 9.93815 17.809244 10 17.955729 10 18.125 C 10 18.294271 9.93815 18.440756 9.814453 18.564453 C 9.690755 18.68815 9.544271 18.75 9.375 18.75 C 9.205729 18.75 9.059244 18.68815 8.935547 18.564453 L 0.214844 9.84375 C 0.143229 9.772136 0.089518 9.700521 0.053711 9.628906 C 0.017904 9.557292 0 9.472656 0 9.375 C 0 9.277344 0.017904 9.192709 0.053711 9.121094 C 0.089518 9.049479 0.143229 8.977865 0.214844 8.90625 L 8.935547 0.185547 C 9.059244 0.06185 9.205729 0 9.375 0 C 9.544271 0 9.690755 0.06185 9.814453 0.185547 C 9.93815 0.309246 10 0.45573 10 0.625 C 10 0.794271 9.93815 0.940756 9.814453 1.064453 L 2.138672 8.75 L 19.375 8.75 C 19.54427 8.75 19.690754 8.81185 19.814453 8.935547 C 19.93815 9.059245 20 9.205729 20 9.375 Z " />
|
||||||
|
</Viewbox>
|
||||||
|
</Button.Content>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
Width="150"
|
||||||
|
Height="50"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Source="{x:Bind SelectedMailProvider.ProviderImage, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
x:Name="DisplayNameTextBox"
|
||||||
|
Grid.Row="1"
|
||||||
|
Header="Display Name"
|
||||||
|
PlaceholderText="eg. John Doe"
|
||||||
|
TextChanged="InputChanged" />
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
x:Name="SpecialImapAddress"
|
||||||
|
Grid.Row="2"
|
||||||
|
Header="E-mail Address"
|
||||||
|
PlaceholderText="eg. johndoe@testmail.com"
|
||||||
|
TextChanged="InputChanged" />
|
||||||
|
|
||||||
|
<PasswordBox
|
||||||
|
x:Name="AppSpecificPassword"
|
||||||
|
Grid.Row="3"
|
||||||
|
Header="App-Specific Password"
|
||||||
|
PasswordChanged="ImapPasswordChanged" />
|
||||||
|
|
||||||
|
<HyperlinkButton
|
||||||
|
Grid.Row="4"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Click="AppSpecificHelpButtonClicked"
|
||||||
|
Content="How do I get app-specific password?" />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Windows.System;
|
||||||
using Windows.UI.Xaml;
|
using Windows.UI.Xaml;
|
||||||
using Windows.UI.Xaml.Controls;
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
@@ -8,6 +11,17 @@ namespace Wino.Core.UWP.Dialogs
|
|||||||
{
|
{
|
||||||
public sealed partial class NewAccountDialog : ContentDialog
|
public sealed partial class NewAccountDialog : ContentDialog
|
||||||
{
|
{
|
||||||
|
private Dictionary<SpecialImapProvider, string> helpingLinks = new Dictionary<SpecialImapProvider, string>()
|
||||||
|
{
|
||||||
|
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
|
||||||
|
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly DependencyProperty IsProviderSelectionVisibleProperty = DependencyProperty.Register(nameof(IsProviderSelectionVisible), typeof(bool), typeof(NewAccountDialog), new PropertyMetadata(true));
|
||||||
|
public static readonly DependencyProperty IsSpecialImapServerPartVisibleProperty = DependencyProperty.Register(nameof(IsSpecialImapServerPartVisible), typeof(bool), typeof(NewAccountDialog), new PropertyMetadata(false));
|
||||||
|
public static readonly DependencyProperty SelectedMailProviderProperty = DependencyProperty.Register(nameof(SelectedMailProvider), typeof(ProviderDetail), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedProviderChanged)));
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets current selected mail provider in the dialog.
|
/// Gets or sets current selected mail provider in the dialog.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -17,7 +31,18 @@ namespace Wino.Core.UWP.Dialogs
|
|||||||
set { SetValue(SelectedMailProviderProperty, value); }
|
set { SetValue(SelectedMailProviderProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty SelectedMailProviderProperty = DependencyProperty.Register(nameof(SelectedMailProvider), typeof(ProviderDetail), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedProviderChanged)));
|
|
||||||
|
public bool IsProviderSelectionVisible
|
||||||
|
{
|
||||||
|
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
|
||||||
|
set { SetValue(IsProviderSelectionVisibleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSpecialImapServerPartVisible
|
||||||
|
{
|
||||||
|
get { return (bool)GetValue(IsSpecialImapServerPartVisibleProperty); }
|
||||||
|
set { SetValue(IsSpecialImapServerPartVisibleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
// List of available mail providers for now.
|
// List of available mail providers for now.
|
||||||
|
|
||||||
@@ -45,16 +70,40 @@ namespace Wino.Core.UWP.Dialogs
|
|||||||
|
|
||||||
private void CreateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
private void CreateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
|
if (IsSpecialImapServerPartVisible)
|
||||||
|
{
|
||||||
|
// Special imap detail input.
|
||||||
|
|
||||||
|
var details = new SpecialImapProviderDetails(SpecialImapAddress.Text.Trim(), AppSpecificPassword.Password.Trim(), DisplayNameTextBox.Text.Trim(), SelectedMailProvider.SpecialImapProvider);
|
||||||
|
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), details);
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Validate();
|
Validate();
|
||||||
|
|
||||||
if (IsSecondaryButtonEnabled)
|
if (IsSecondaryButtonEnabled)
|
||||||
{
|
{
|
||||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim());
|
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None)
|
||||||
Hide();
|
{
|
||||||
|
// This step requires app-sepcific password login for some providers.
|
||||||
|
args.Cancel = true;
|
||||||
|
|
||||||
|
IsProviderSelectionVisible = false;
|
||||||
|
IsSpecialImapServerPartVisible = true;
|
||||||
|
|
||||||
|
Validate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), null);
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AccountNameChanged(object sender, TextChangedEventArgs e) => Validate();
|
private void InputChanged(object sender, TextChangedEventArgs e) => Validate();
|
||||||
private void SenderNameChanged(object sender, TextChangedEventArgs e) => Validate();
|
private void SenderNameChanged(object sender, TextChangedEventArgs e) => Validate();
|
||||||
|
|
||||||
private void Validate()
|
private void Validate()
|
||||||
@@ -68,7 +117,10 @@ namespace Wino.Core.UWP.Dialogs
|
|||||||
{
|
{
|
||||||
bool shouldEnable = SelectedMailProvider != null
|
bool shouldEnable = SelectedMailProvider != null
|
||||||
&& SelectedMailProvider.IsSupported
|
&& SelectedMailProvider.IsSupported
|
||||||
&& !string.IsNullOrEmpty(AccountNameTextbox.Text);
|
&& !string.IsNullOrEmpty(AccountNameTextbox.Text)
|
||||||
|
&& (IsSpecialImapServerPartVisible ? (!string.IsNullOrEmpty(AppSpecificPassword.Password)
|
||||||
|
&& !string.IsNullOrEmpty(DisplayNameTextBox.Text)
|
||||||
|
&& EmailValidation.EmailValidator.Validate(SpecialImapAddress.Text)) : true);
|
||||||
|
|
||||||
IsPrimaryButtonEnabled = shouldEnable;
|
IsPrimaryButtonEnabled = shouldEnable;
|
||||||
}
|
}
|
||||||
@@ -79,5 +131,22 @@ namespace Wino.Core.UWP.Dialogs
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate();
|
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate();
|
||||||
|
|
||||||
|
private void BackClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
IsSpecialImapServerPartVisible = false;
|
||||||
|
IsProviderSelectionVisible = true;
|
||||||
|
|
||||||
|
Validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
|
||||||
|
|
||||||
|
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var helpUrl = helpingLinks[SelectedMailProvider.SpecialImapProvider];
|
||||||
|
|
||||||
|
await Launcher.LaunchUriAsync(new Uri(helpUrl));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Windows.UI.Xaml.Controls.Primitives;
|
|||||||
using Windows.UI.Xaml.Markup;
|
using Windows.UI.Xaml.Markup;
|
||||||
using Windows.UI.Xaml.Media;
|
using Windows.UI.Xaml.Media;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.UWP.Controls;
|
using Wino.Core.UWP.Controls;
|
||||||
@@ -262,17 +263,31 @@ namespace Wino.Helpers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WinoIconGlyph GetProviderIcon(MailProviderType providerType)
|
|
||||||
|
public static WinoIconGlyph GetProviderIcon(MailProviderType providerType, SpecialImapProvider specialImapProvider)
|
||||||
{
|
{
|
||||||
return providerType switch
|
if (specialImapProvider == SpecialImapProvider.None)
|
||||||
{
|
{
|
||||||
MailProviderType.Outlook => WinoIconGlyph.Microsoft,
|
return providerType switch
|
||||||
MailProviderType.Gmail => WinoIconGlyph.Google,
|
{
|
||||||
MailProviderType.Office365 => WinoIconGlyph.Microsoft,
|
MailProviderType.Outlook => WinoIconGlyph.Microsoft,
|
||||||
MailProviderType.IMAP4 => WinoIconGlyph.IMAP,
|
MailProviderType.Gmail => WinoIconGlyph.Google,
|
||||||
_ => WinoIconGlyph.None,
|
MailProviderType.IMAP4 => WinoIconGlyph.IMAP,
|
||||||
};
|
_ => WinoIconGlyph.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return specialImapProvider switch
|
||||||
|
{
|
||||||
|
SpecialImapProvider.iCloud => WinoIconGlyph.Apple,
|
||||||
|
SpecialImapProvider.Yahoo => WinoIconGlyph.Yahoo,
|
||||||
|
_ => WinoIconGlyph.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
public static WinoIconGlyph GetProviderIcon(MailAccount account)
|
||||||
|
=> GetProviderIcon(account.ProviderType, account.SpecialImapProvider);
|
||||||
|
|
||||||
public static Geometry GetPathGeometry(string pathMarkup)
|
public static Geometry GetPathGeometry(string pathMarkup)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ namespace Wino.Core.UWP.Services
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IAccountCreationDialog GetAccountCreationDialog(MailProviderType type)
|
public virtual IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult)
|
||||||
{
|
{
|
||||||
return new AccountCreationDialog
|
return new AccountCreationDialog
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Wino.Core.Integration.Json;
|
|||||||
using Wino.Messaging;
|
using Wino.Messaging;
|
||||||
using Wino.Messaging.Client.Connection;
|
using Wino.Messaging.Client.Connection;
|
||||||
using Wino.Messaging.Enums;
|
using Wino.Messaging.Enums;
|
||||||
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.UWP.Services
|
namespace Wino.Core.UWP.Services
|
||||||
@@ -256,6 +257,9 @@ namespace Wino.Core.UWP.Services
|
|||||||
case nameof(CopyAuthURLRequested):
|
case nameof(CopyAuthURLRequested):
|
||||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.CopyAuthURLRequested));
|
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.CopyAuthURLRequested));
|
||||||
break;
|
break;
|
||||||
|
case nameof(NewMailSynchronizationRequested):
|
||||||
|
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<NewMailSynchronizationRequested>(messageJson));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception("Invalid data type name passed to client.");
|
throw new Exception("Invalid data type name passed to client.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
Header="{x:Bind Account.Name}"
|
Header="{x:Bind Account.Name}"
|
||||||
IsClickEnabled="True">
|
IsClickEnabled="True">
|
||||||
<winuiControls:SettingsCard.HeaderIcon>
|
<winuiControls:SettingsCard.HeaderIcon>
|
||||||
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(ProviderDetail.Type)}" />
|
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Account)}" />
|
||||||
</winuiControls:SettingsCard.HeaderIcon>
|
</winuiControls:SettingsCard.HeaderIcon>
|
||||||
</winuiControls:SettingsCard>
|
</winuiControls:SettingsCard>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<None Remove="Assets\FileTypes\type_rar.png" />
|
<None Remove="Assets\FileTypes\type_rar.png" />
|
||||||
<None Remove="Assets\FileTypes\type_video.png" />
|
<None Remove="Assets\FileTypes\type_video.png" />
|
||||||
<None Remove="Assets\Providers\Gmail.png" />
|
<None Remove="Assets\Providers\Gmail.png" />
|
||||||
|
<None Remove="Assets\Providers\iCloud.png" />
|
||||||
<None Remove="Assets\Providers\IMAP4.png" />
|
<None Remove="Assets\Providers\IMAP4.png" />
|
||||||
<None Remove="Assets\Providers\Office 365.png" />
|
<None Remove="Assets\Providers\Office 365.png" />
|
||||||
<None Remove="Assets\Providers\Outlook.png" />
|
<None Remove="Assets\Providers\Outlook.png" />
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
<Content Include="Assets\FileTypes\type_rar.png" />
|
<Content Include="Assets\FileTypes\type_rar.png" />
|
||||||
<Content Include="Assets\FileTypes\type_video.png" />
|
<Content Include="Assets\FileTypes\type_video.png" />
|
||||||
<Content Include="Assets\Providers\Gmail.png" />
|
<Content Include="Assets\Providers\Gmail.png" />
|
||||||
|
<Content Include="Assets\Providers\iCloud.png" />
|
||||||
<Content Include="Assets\Providers\IMAP4.png" />
|
<Content Include="Assets\Providers\IMAP4.png" />
|
||||||
<Content Include="Assets\Providers\Office 365.png" />
|
<Content Include="Assets\Providers\Office 365.png" />
|
||||||
<Content Include="Assets\Providers\Outlook.png" />
|
<Content Include="Assets\Providers\Outlook.png" />
|
||||||
@@ -99,6 +101,7 @@
|
|||||||
<PackageReference Include="Microsoft.AppCenter.Analytics" />
|
<PackageReference Include="Microsoft.AppCenter.Analytics" />
|
||||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" />
|
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" />
|
||||||
<PackageReference Include="Win2D.uwp" />
|
<PackageReference Include="Win2D.uwp" />
|
||||||
|
<PackageReference Include="EmailValidation" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Wino.Authentication;
|
|||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Integration.Processors;
|
using Wino.Core.Integration.Processors;
|
||||||
using Wino.Core.Services;
|
using Wino.Core.Services;
|
||||||
|
using Wino.Core.Synchronizers.ImapSync;
|
||||||
|
|
||||||
namespace Wino.Core
|
namespace Wino.Core
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,11 @@ namespace Wino.Core
|
|||||||
services.AddTransient<IUnsubscriptionService, UnsubscriptionService>();
|
services.AddTransient<IUnsubscriptionService, UnsubscriptionService>();
|
||||||
services.AddTransient<IOutlookAuthenticator, OutlookAuthenticator>();
|
services.AddTransient<IOutlookAuthenticator, OutlookAuthenticator>();
|
||||||
services.AddTransient<IGmailAuthenticator, GmailAuthenticator>();
|
services.AddTransient<IGmailAuthenticator, GmailAuthenticator>();
|
||||||
|
|
||||||
|
services.AddTransient<IImapSynchronizationStrategyProvider, ImapSynchronizationStrategyProvider>();
|
||||||
|
services.AddTransient<CondstoreSynchronizer>();
|
||||||
|
services.AddTransient<QResyncSynchronizer>();
|
||||||
|
services.AddTransient<UidBasedSynchronizer>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ namespace Wino.Core.Integration
|
|||||||
/// Provides a pooling mechanism for ImapClient.
|
/// Provides a pooling mechanism for ImapClient.
|
||||||
/// Makes sure that we don't have too many connections to the server.
|
/// Makes sure that we don't have too many connections to the server.
|
||||||
/// Rents a connected & authenticated client from the pool all the time.
|
/// Rents a connected & authenticated client from the pool all the time.
|
||||||
/// TODO: Keeps the clients alive by sending NOOP command periodically.
|
|
||||||
/// TODO: Listens to the Inbox folder for new messages.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
|
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
|
||||||
public class ImapClientPool : IDisposable
|
public class ImapClientPool : IDisposable
|
||||||
@@ -48,14 +46,16 @@ namespace Wino.Core.Integration
|
|||||||
|
|
||||||
public bool ThrowOnSSLHandshakeCallback { get; set; }
|
public bool ThrowOnSSLHandshakeCallback { get; set; }
|
||||||
public ImapClientPoolOptions ImapClientPoolOptions { get; }
|
public ImapClientPoolOptions ImapClientPoolOptions { get; }
|
||||||
|
internal WinoImapClient IdleClient { get; set; }
|
||||||
|
|
||||||
private readonly int MinimumPoolSize = 5;
|
private readonly int MinimumPoolSize = 5;
|
||||||
|
|
||||||
private readonly ConcurrentStack<ImapClient> _clients = [];
|
private readonly ConcurrentStack<IImapClient> _clients = [];
|
||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
private readonly CustomServerInformation _customServerInformation;
|
private readonly CustomServerInformation _customServerInformation;
|
||||||
private readonly Stream _protocolLogStream;
|
private readonly Stream _protocolLogStream;
|
||||||
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
|
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
|
||||||
|
private bool _disposedValue;
|
||||||
|
|
||||||
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
|
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,7 @@ namespace Wino.Core.Integration
|
|||||||
/// Reconnects and reauthenticates if necessary.
|
/// Reconnects and reauthenticates if necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
|
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
|
||||||
private async Task EnsureCapabilitiesAsync(ImapClient client, bool isCreatedNew)
|
private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -84,6 +84,9 @@ namespace Wino.Core.Integration
|
|||||||
|
|
||||||
if ((isCreatedNew || isReconnected) && client.IsConnected)
|
if ((isCreatedNew || isReconnected) && client.IsConnected)
|
||||||
{
|
{
|
||||||
|
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
|
||||||
|
await client.CompressAsync();
|
||||||
|
|
||||||
// Identify if the server supports ID extension.
|
// Identify if the server supports ID extension.
|
||||||
// Some servers require it pre-authentication, some post-authentication.
|
// Some servers require it pre-authentication, some post-authentication.
|
||||||
// We'll observe the response here and do it after authentication if needed.
|
// We'll observe the response here and do it after authentication if needed.
|
||||||
@@ -113,10 +116,11 @@ namespace Wino.Core.Integration
|
|||||||
|
|
||||||
// Activate post-auth capabilities.
|
// Activate post-auth capabilities.
|
||||||
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
|
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
|
||||||
await client.EnableQuickResyncAsync();
|
{
|
||||||
|
await client.EnableQuickResyncAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
|
if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true;
|
||||||
await client.CompressAsync();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -145,11 +149,11 @@ namespace Wino.Core.Integration
|
|||||||
return reader.ReadToEnd();
|
return reader.ReadToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImapClient> GetClientAsync()
|
public async Task<IImapClient> GetClientAsync()
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync();
|
await _semaphore.WaitAsync();
|
||||||
|
|
||||||
if (_clients.TryPop(out ImapClient item))
|
if (_clients.TryPop(out IImapClient item))
|
||||||
{
|
{
|
||||||
await EnsureCapabilitiesAsync(item, false);
|
await EnsureCapabilitiesAsync(item, false);
|
||||||
|
|
||||||
@@ -163,20 +167,24 @@ namespace Wino.Core.Integration
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Release(ImapClient item, bool destroyClient = false)
|
public void Release(IImapClient item, bool destroyClient = false)
|
||||||
{
|
{
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
if (destroyClient)
|
if (destroyClient)
|
||||||
{
|
{
|
||||||
lock (item.SyncRoot)
|
if (item.IsConnected)
|
||||||
{
|
{
|
||||||
item.Disconnect(true);
|
lock (item.SyncRoot)
|
||||||
|
{
|
||||||
|
item.Disconnect(quit: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clients.TryPop(out _);
|
||||||
item.Dispose();
|
item.Dispose();
|
||||||
}
|
}
|
||||||
else
|
else if (!_disposedValue)
|
||||||
{
|
{
|
||||||
_clients.Push(item);
|
_clients.Push(item);
|
||||||
}
|
}
|
||||||
@@ -185,23 +193,15 @@ namespace Wino.Core.Integration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DestroyClient(ImapClient client)
|
private IImapClient CreateNewClient()
|
||||||
{
|
{
|
||||||
if (client == null) return;
|
WinoImapClient client = null;
|
||||||
|
|
||||||
client.Disconnect(true);
|
|
||||||
client.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImapClient CreateNewClient()
|
|
||||||
{
|
|
||||||
ImapClient client = null;
|
|
||||||
|
|
||||||
// Make sure to create a ImapClient with a protocol logger if enabled.
|
// Make sure to create a ImapClient with a protocol logger if enabled.
|
||||||
|
|
||||||
client = _protocolLogStream != null
|
client = _protocolLogStream != null
|
||||||
? new ImapClient(new ProtocolLogger(_protocolLogStream))
|
? new WinoImapClient(new ProtocolLogger(_protocolLogStream))
|
||||||
: new ImapClient();
|
: new WinoImapClient();
|
||||||
|
|
||||||
HttpProxyClient proxyClient = null;
|
HttpProxyClient proxyClient = null;
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ namespace Wino.Core.Integration
|
|||||||
|
|
||||||
client.ProxyClient = proxyClient;
|
client.ProxyClient = proxyClient;
|
||||||
|
|
||||||
_logger.Debug("Created new ImapClient. Current clients: {Count}", _clients.Count);
|
_logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count);
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ namespace Wino.Core.Integration
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <returns>True if the connection is newly established.</returns>
|
/// <returns>True if the connection is newly established.</returns>
|
||||||
public async Task<bool> EnsureConnectedAsync(ImapClient client)
|
public async Task<bool> EnsureConnectedAsync(IImapClient client)
|
||||||
{
|
{
|
||||||
if (client.IsConnected) return false;
|
if (client.IsConnected) return false;
|
||||||
|
|
||||||
@@ -263,8 +263,20 @@ namespace Wino.Core.Integration
|
|||||||
{
|
{
|
||||||
if (_protocolLogStream == null) return;
|
if (_protocolLogStream == null) return;
|
||||||
|
|
||||||
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
|
try
|
||||||
_protocolLogStream.Write(messageBytes, 0, messageBytes.Length);
|
{
|
||||||
|
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)
|
bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||||
@@ -281,7 +293,7 @@ namespace Wino.Core.Integration
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureAuthenticatedAsync(ImapClient client)
|
public async Task EnsureAuthenticatedAsync(IImapClient client)
|
||||||
{
|
{
|
||||||
if (client.IsAuthenticated) return;
|
if (client.IsAuthenticated) return;
|
||||||
|
|
||||||
@@ -319,27 +331,38 @@ namespace Wino.Core.Integration
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_clients.ForEach(client =>
|
Dispose(disposing: true);
|
||||||
{
|
GC.SuppressFinalize(this);
|
||||||
lock (client.SyncRoot)
|
|
||||||
{
|
|
||||||
client.Disconnect(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_clients.ForEach(client =>
|
|
||||||
{
|
|
||||||
client.Dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
_clients.Clear();
|
|
||||||
|
|
||||||
if (_protocolLogStream != null)
|
|
||||||
{
|
|
||||||
_protocolLogStream.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
Wino.Core/Integration/WinoImapClient.cs
Normal file
60
Wino.Core/Integration/WinoImapClient.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using MailKit;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
HookEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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,9 +0,0 @@
|
|||||||
using MailKit;
|
|
||||||
|
|
||||||
namespace Wino.Core.Mime
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Encapsulates all required information to create a MimeMessage for IMAP synchronizer.
|
|
||||||
/// </summary>
|
|
||||||
public record ImapMessageCreationPackage(IMessageSummary MessageSummary, IMailFolder MailFolder);
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,10 @@ namespace Wino.Core.Requests.Bundles
|
|||||||
{
|
{
|
||||||
public class ImapRequest
|
public class ImapRequest
|
||||||
{
|
{
|
||||||
public Func<ImapClient, IRequestBase, Task> IntegratorTask { get; }
|
public Func<IImapClient, IRequestBase, Task> IntegratorTask { get; }
|
||||||
public IRequestBase Request { get; }
|
public IRequestBase Request { get; }
|
||||||
|
|
||||||
public ImapRequest(Func<ImapClient, IRequestBase, Task> integratorTask, IRequestBase request)
|
public ImapRequest(Func<IImapClient, IRequestBase, Task> integratorTask, IRequestBase request)
|
||||||
{
|
{
|
||||||
IntegratorTask = integratorTask;
|
IntegratorTask = integratorTask;
|
||||||
Request = request;
|
Request = request;
|
||||||
@@ -19,7 +19,7 @@ namespace Wino.Core.Requests.Bundles
|
|||||||
|
|
||||||
public class ImapRequest<TRequestBaseType> : ImapRequest where TRequestBaseType : IRequestBase
|
public class ImapRequest<TRequestBaseType> : ImapRequest where TRequestBaseType : IRequestBase
|
||||||
{
|
{
|
||||||
public ImapRequest(Func<ImapClient, TRequestBaseType, Task> integratorTask, TRequestBaseType request)
|
public ImapRequest(Func<IImapClient, TRequestBaseType, Task> integratorTask, TRequestBaseType request)
|
||||||
: base((client, request) => integratorTask(client, (TRequestBaseType)request), request)
|
: base((client, request) => integratorTask(client, (TRequestBaseType)request), request)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Wino.Core.Requests.Folder
|
|||||||
{
|
{
|
||||||
public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDelete) : FolderRequestBase(Folder, FolderSynchronizerOperation.EmptyFolder), ICustomFolderSynchronizationRequest
|
public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDelete) : FolderRequestBase(Folder, FolderSynchronizerOperation.EmptyFolder), ICustomFolderSynchronizationRequest
|
||||||
{
|
{
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
{
|
{
|
||||||
foreach (var item in MailsToDelete)
|
foreach (var item in MailsToDelete)
|
||||||
|
|||||||
@@ -32,5 +32,7 @@ namespace Wino.Core.Requests.Folder
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Guid> SynchronizationFolderIds => [Folder.Id];
|
public List<Guid> SynchronizationFolderIds => [Folder.Id];
|
||||||
|
|
||||||
|
public bool ExcludeMustHaveFolders => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Wino.Core.Requests.Mail
|
|||||||
public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder = null)
|
public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder FromFolder, MailItemFolder ToFolder = null)
|
||||||
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
||||||
{
|
{
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
public List<Guid> SynchronizationFolderIds
|
public List<Guid> SynchronizationFolderIds
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace Wino.Core.Requests.Mail
|
|||||||
{
|
{
|
||||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
||||||
|
|
||||||
|
public bool ExcludeMustHaveFolders => true;
|
||||||
|
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace Wino.Core.Requests.Mail
|
|||||||
: MailRequestBase(DraftPreperationRequest.CreatedLocalDraftCopy),
|
: MailRequestBase(DraftPreperationRequest.CreatedLocalDraftCopy),
|
||||||
ICustomFolderSynchronizationRequest
|
ICustomFolderSynchronizationRequest
|
||||||
{
|
{
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
|
|
||||||
public List<Guid> SynchronizationFolderIds =>
|
public List<Guid> SynchronizationFolderIds =>
|
||||||
[
|
[
|
||||||
DraftPreperationRequest.CreatedLocalDraftCopy.AssignedFolder.Id
|
DraftPreperationRequest.CreatedLocalDraftCopy.AssignedFolder.Id
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace Wino.Core.Requests.Mail
|
|||||||
ICustomFolderSynchronizationRequest
|
ICustomFolderSynchronizationRequest
|
||||||
{
|
{
|
||||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace Wino.Core.Requests.Mail
|
|||||||
|
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead;
|
||||||
|
|
||||||
|
public bool ExcludeMustHaveFolders => true;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
{
|
{
|
||||||
Item.IsRead = IsRead;
|
Item.IsRead = IsRead;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Wino.Core.Requests.Mail
|
|||||||
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
: MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
||||||
{
|
{
|
||||||
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
|
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ namespace Wino.Core.Requests.Mail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ExcludeMustHaveFolders => false;
|
||||||
|
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Send;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Send;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ namespace Wino.Core.Services
|
|||||||
return providerType switch
|
return providerType switch
|
||||||
{
|
{
|
||||||
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
||||||
MailProviderType.Office365 => new Office365Authenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
|
||||||
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
|
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
|
||||||
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Wino.Core.Services
|
|||||||
private bool isInitialized = false;
|
private bool isInitialized = false;
|
||||||
|
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider;
|
||||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||||
@@ -28,6 +29,7 @@ namespace Wino.Core.Services
|
|||||||
IOutlookAuthenticator outlookAuthenticator,
|
IOutlookAuthenticator outlookAuthenticator,
|
||||||
IGmailAuthenticator gmailAuthenticator,
|
IGmailAuthenticator gmailAuthenticator,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
|
IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider,
|
||||||
IApplicationConfiguration applicationConfiguration)
|
IApplicationConfiguration applicationConfiguration)
|
||||||
{
|
{
|
||||||
_outlookChangeProcessor = outlookChangeProcessor;
|
_outlookChangeProcessor = outlookChangeProcessor;
|
||||||
@@ -36,6 +38,7 @@ namespace Wino.Core.Services
|
|||||||
_outlookAuthenticator = outlookAuthenticator;
|
_outlookAuthenticator = outlookAuthenticator;
|
||||||
_gmailAuthenticator = gmailAuthenticator;
|
_gmailAuthenticator = gmailAuthenticator;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
|
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
|
||||||
_applicationConfiguration = applicationConfiguration;
|
_applicationConfiguration = applicationConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ namespace Wino.Core.Services
|
|||||||
{
|
{
|
||||||
synchronizer = CreateNewSynchronizer(account);
|
synchronizer = CreateNewSynchronizer(account);
|
||||||
|
|
||||||
|
|
||||||
return await GetAccountSynchronizerAsync(accountId);
|
return await GetAccountSynchronizerAsync(accountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,12 +69,11 @@ namespace Wino.Core.Services
|
|||||||
switch (providerType)
|
switch (providerType)
|
||||||
{
|
{
|
||||||
case Domain.Enums.MailProviderType.Outlook:
|
case Domain.Enums.MailProviderType.Outlook:
|
||||||
case Domain.Enums.MailProviderType.Office365:
|
|
||||||
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor);
|
return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor);
|
||||||
case Domain.Enums.MailProviderType.Gmail:
|
case Domain.Enums.MailProviderType.Gmail:
|
||||||
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor);
|
return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor);
|
||||||
case Domain.Enums.MailProviderType.IMAP4:
|
case Domain.Enums.MailProviderType.IMAP4:
|
||||||
return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _applicationConfiguration);
|
return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,6 +85,12 @@ namespace Wino.Core.Services
|
|||||||
{
|
{
|
||||||
var synchronizer = CreateIntegratorWithDefaultProcessor(account);
|
var synchronizer = CreateIntegratorWithDefaultProcessor(account);
|
||||||
|
|
||||||
|
if (synchronizer is IImapSynchronizer imapSynchronizer)
|
||||||
|
{
|
||||||
|
// Start the idle client for IMAP synchronizer.
|
||||||
|
_ = imapSynchronizer.StartIdleClientAsync();
|
||||||
|
}
|
||||||
|
|
||||||
synchronizerCache.Add(synchronizer);
|
synchronizerCache.Add(synchronizer);
|
||||||
|
|
||||||
return synchronizer;
|
return synchronizer;
|
||||||
@@ -100,5 +109,18 @@ namespace Wino.Core.Services
|
|||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSynchronizerAsync(Guid accountId)
|
||||||
|
{
|
||||||
|
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
|
||||||
|
|
||||||
|
if (synchronizer != null)
|
||||||
|
{
|
||||||
|
// Stop the current synchronization.
|
||||||
|
await synchronizer.KillSynchronizerAsync();
|
||||||
|
|
||||||
|
synchronizerCache.Remove(synchronizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace Wino.Core.Services
|
|||||||
{
|
{
|
||||||
return mailProviderType switch
|
return mailProviderType switch
|
||||||
{
|
{
|
||||||
MailProviderType.Outlook or MailProviderType.Office365 => _outlookThreadingStrategy,
|
MailProviderType.Outlook => _outlookThreadingStrategy,
|
||||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||||
_ => _imapThreadStrategy,
|
_ => _imapThreadStrategy,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -163,8 +163,7 @@ namespace Wino.Core.Services
|
|||||||
MailItemFolder archiveFolder = null;
|
MailItemFolder archiveFolder = null;
|
||||||
|
|
||||||
bool shouldRequireArchiveFolder = mailItem.AssignedAccount.ProviderType == MailProviderType.Outlook
|
bool shouldRequireArchiveFolder = mailItem.AssignedAccount.ProviderType == MailProviderType.Outlook
|
||||||
|| mailItem.AssignedAccount.ProviderType == MailProviderType.IMAP4
|
|| mailItem.AssignedAccount.ProviderType == MailProviderType.IMAP4;
|
||||||
|| mailItem.AssignedAccount.ProviderType == MailProviderType.Office365;
|
|
||||||
|
|
||||||
if (shouldRequireArchiveFolder)
|
if (shouldRequireArchiveFolder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ namespace Wino.Core.Synchronizers
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
|
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
// TODO: What if account is deleted during synchronization?
|
|
||||||
public bool CancelActiveSynchronization() => true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes remote mail account profile if possible.
|
/// Refreshes remote mail account profile if possible.
|
||||||
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
|
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
|
||||||
|
|||||||
@@ -1193,5 +1193,15 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public override async Task KillSynchronizerAsync()
|
||||||
|
{
|
||||||
|
await base.KillSynchronizerAsync();
|
||||||
|
|
||||||
|
_gmailService.Dispose();
|
||||||
|
_peopleService.Dispose();
|
||||||
|
_calendarService.Dispose();
|
||||||
|
_googleHttpClient.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs
Normal file
131
Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MailKit;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using MailKit.Search;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Exceptions;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var localHighestModSeq = (ulong)folder.HighestModeSeq;
|
||||||
|
|
||||||
|
bool isInitialSynchronization = localHighestModSeq == 0;
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MailKit;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using MailKit.Search;
|
||||||
|
using MoreLinq;
|
||||||
|
using Serilog;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
FolderService = folderService;
|
||||||
|
MailService = mailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
// This is upsert. We are not interested in updated mails.
|
||||||
|
if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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 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)
|
||||||
|
{
|
||||||
|
var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken);
|
||||||
|
var deletedUids = allUids.Except(remoteAllUids).ToList();
|
||||||
|
|
||||||
|
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using MailKit.Net.Imap;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Integration;
|
||||||
|
|
||||||
|
namespace Wino.Core.Synchronizers.ImapSync
|
||||||
|
{
|
||||||
|
internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider
|
||||||
|
{
|
||||||
|
private readonly QResyncSynchronizer _qResyncSynchronizer;
|
||||||
|
private readonly CondstoreSynchronizer _condstoreSynchronizer;
|
||||||
|
private readonly UidBasedSynchronizer _uidBasedSynchronizer;
|
||||||
|
|
||||||
|
public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer)
|
||||||
|
{
|
||||||
|
_qResyncSynchronizer = qResyncSynchronizer;
|
||||||
|
_condstoreSynchronizer = condstoreSynchronizer;
|
||||||
|
_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));
|
||||||
|
|
||||||
|
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer;
|
||||||
|
if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer;
|
||||||
|
|
||||||
|
return _uidBasedSynchronizer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs
Normal file
121
Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MailKit;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using MailKit.Search;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Exceptions;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
|
||||||
|
bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity;
|
||||||
|
|
||||||
|
if (!isCacheValid)
|
||||||
|
{
|
||||||
|
// TODO: Remove all local data.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
remoteFolder.MessagesVanished -= OnMessagesVanished;
|
||||||
|
remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged;
|
||||||
|
|
||||||
|
if (remoteFolder.IsOpen)
|
||||||
|
{
|
||||||
|
await remoteFolder.CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs
Normal file
81
Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MailKit;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using MailKit.Search;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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 != null)
|
||||||
|
{
|
||||||
|
if (remoteFolder.IsOpen)
|
||||||
|
{
|
||||||
|
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadedMessageIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -7,8 +8,6 @@ using System.Threading.Tasks;
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using MailKit;
|
using MailKit;
|
||||||
using MailKit.Net.Imap;
|
using MailKit.Net.Imap;
|
||||||
using MailKit.Search;
|
|
||||||
using MimeKit;
|
|
||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
@@ -22,72 +21,55 @@ using Wino.Core.Domain.Models.Synchronization;
|
|||||||
using Wino.Core.Extensions;
|
using Wino.Core.Extensions;
|
||||||
using Wino.Core.Integration;
|
using Wino.Core.Integration;
|
||||||
using Wino.Core.Integration.Processors;
|
using Wino.Core.Integration.Processors;
|
||||||
using Wino.Core.Mime;
|
|
||||||
using Wino.Core.Requests.Bundles;
|
using Wino.Core.Requests.Bundles;
|
||||||
using Wino.Core.Requests.Folder;
|
using Wino.Core.Requests.Folder;
|
||||||
using Wino.Core.Requests.Mail;
|
using Wino.Core.Requests.Mail;
|
||||||
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
using Wino.Services.Extensions;
|
using Wino.Services.Extensions;
|
||||||
|
|
||||||
namespace Wino.Core.Synchronizers.Mail
|
namespace Wino.Core.Synchronizers.Mail
|
||||||
{
|
{
|
||||||
public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreationPackage, object>
|
public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreationPackage, object>, IImapSynchronizer
|
||||||
{
|
{
|
||||||
private CancellationTokenSource idleDoneToken;
|
[Obsolete("N/A")]
|
||||||
private CancellationTokenSource cancelInboxListeningToken = new CancellationTokenSource();
|
public override uint BatchModificationSize => 1000;
|
||||||
|
public override uint InitialMessageDownloadCountPerFolder => 500;
|
||||||
|
|
||||||
private IMailFolder inboxFolder;
|
#region Idle Implementation
|
||||||
|
|
||||||
|
private CancellationTokenSource idleCancellationTokenSource;
|
||||||
|
private CancellationTokenSource idleDoneTokenSource;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private readonly ILogger _logger = Log.ForContext<ImapSynchronizer>();
|
private readonly ILogger _logger = Log.ForContext<ImapSynchronizer>();
|
||||||
private readonly ImapClientPool _clientPool;
|
private readonly ImapClientPool _clientPool;
|
||||||
private readonly IImapChangeProcessor _imapChangeProcessor;
|
private readonly IImapChangeProcessor _imapChangeProcessor;
|
||||||
|
private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider;
|
||||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||||
|
|
||||||
// Minimum summary items to Fetch for mail synchronization from IMAP.
|
|
||||||
private readonly MessageSummaryItems mailSynchronizationFlags =
|
|
||||||
MessageSummaryItems.Flags |
|
|
||||||
MessageSummaryItems.UniqueId |
|
|
||||||
MessageSummaryItems.ThreadId |
|
|
||||||
MessageSummaryItems.EmailId |
|
|
||||||
MessageSummaryItems.Headers |
|
|
||||||
MessageSummaryItems.PreviewText |
|
|
||||||
MessageSummaryItems.GMailThreadId |
|
|
||||||
MessageSummaryItems.References |
|
|
||||||
MessageSummaryItems.ModSeq;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timer that keeps the <see cref="InboxClient"/> alive for the lifetime of the pool.
|
|
||||||
/// Sends NOOP command to the server periodically.
|
|
||||||
/// </summary>
|
|
||||||
private Timer _noOpTimer;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ImapClient that keeps the Inbox folder opened all the time for listening notifications.
|
|
||||||
/// </summary>
|
|
||||||
private ImapClient _inboxIdleClient;
|
|
||||||
|
|
||||||
public override uint BatchModificationSize => 1000;
|
|
||||||
public override uint InitialMessageDownloadCountPerFolder => 250;
|
|
||||||
|
|
||||||
public ImapSynchronizer(MailAccount account,
|
public ImapSynchronizer(MailAccount account,
|
||||||
IImapChangeProcessor imapChangeProcessor,
|
IImapChangeProcessor imapChangeProcessor,
|
||||||
|
IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider,
|
||||||
IApplicationConfiguration applicationConfiguration) : base(account)
|
IApplicationConfiguration applicationConfiguration) : base(account)
|
||||||
{
|
{
|
||||||
// Create client pool with account protocol log.
|
// Create client pool with account protocol log.
|
||||||
_imapChangeProcessor = imapChangeProcessor;
|
_imapChangeProcessor = imapChangeProcessor;
|
||||||
|
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
|
||||||
_applicationConfiguration = applicationConfiguration;
|
_applicationConfiguration = applicationConfiguration;
|
||||||
|
|
||||||
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, CreateAccountProtocolLogFileStream());
|
var protocolLogStream = CreateAccountProtocolLogFileStream();
|
||||||
|
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
|
||||||
|
|
||||||
_clientPool = new ImapClientPool(poolOptions);
|
_clientPool = new ImapClientPool(poolOptions);
|
||||||
idleDoneToken = new CancellationTokenSource();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stream CreateAccountProtocolLogFileStream()
|
private Stream CreateAccountProtocolLogFileStream()
|
||||||
{
|
{
|
||||||
if (Account == null) throw new ArgumentNullException(nameof(Account));
|
if (Account == null) throw new ArgumentNullException(nameof(Account));
|
||||||
|
|
||||||
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}.log");
|
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log");
|
||||||
|
|
||||||
// Each session should start a new log.
|
// Each session should start a new log.
|
||||||
if (File.Exists(logFile)) File.Delete(logFile);
|
if (File.Exists(logFile)) File.Delete(logFile);
|
||||||
@@ -95,136 +77,10 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
return new FileStream(logFile, FileMode.CreateNew);
|
return new FileStream(logFile, FileMode.CreateNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
// private async void NoOpTimerTriggered(object state) => await AwaitInboxIdleAsync();
|
|
||||||
|
|
||||||
private async Task AwaitInboxIdleAsync()
|
|
||||||
{
|
|
||||||
if (_inboxIdleClient == null)
|
|
||||||
{
|
|
||||||
_logger.Warning("InboxClient is null. Cannot send NOOP command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _clientPool.EnsureConnectedAsync(_inboxIdleClient);
|
|
||||||
await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (inboxFolder == null)
|
|
||||||
{
|
|
||||||
inboxFolder = _inboxIdleClient.Inbox;
|
|
||||||
await inboxFolder.OpenAsync(FolderAccess.ReadOnly, cancelInboxListeningToken.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
idleDoneToken = new CancellationTokenSource();
|
|
||||||
|
|
||||||
await _inboxIdleClient.IdleAsync(idleDoneToken.Token, cancelInboxListeningToken.Token);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
idleDoneToken.Dispose();
|
|
||||||
idleDoneToken = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StopInboxListeningAsync()
|
|
||||||
{
|
|
||||||
if (inboxFolder != null)
|
|
||||||
{
|
|
||||||
inboxFolder.CountChanged -= InboxFolderCountChanged;
|
|
||||||
inboxFolder.MessageExpunged -= InboxFolderMessageExpunged;
|
|
||||||
inboxFolder.MessageFlagsChanged -= InboxFolderMessageFlagsChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_noOpTimer != null)
|
|
||||||
{
|
|
||||||
_noOpTimer.Dispose();
|
|
||||||
_noOpTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idleDoneToken != null)
|
|
||||||
{
|
|
||||||
idleDoneToken.Cancel();
|
|
||||||
idleDoneToken.Dispose();
|
|
||||||
idleDoneToken = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_inboxIdleClient != null)
|
|
||||||
{
|
|
||||||
await _inboxIdleClient.DisconnectAsync(true);
|
|
||||||
_inboxIdleClient.Dispose();
|
|
||||||
_inboxIdleClient = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to connect & authenticate with the given credentials.
|
|
||||||
/// Prepares synchronizer for active listening of Inbox folder.
|
|
||||||
/// </summary>
|
|
||||||
public async Task StartInboxListeningAsync()
|
|
||||||
{
|
|
||||||
_inboxIdleClient = await _clientPool.GetClientAsync();
|
|
||||||
|
|
||||||
// Run it every 8 minutes after 1 minute delay.
|
|
||||||
// _noOpTimer = new Timer(NoOpTimerTriggered, null, 60000, 8 * 60 * 1000);
|
|
||||||
|
|
||||||
await _clientPool.EnsureConnectedAsync(_inboxIdleClient);
|
|
||||||
await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient);
|
|
||||||
|
|
||||||
if (!_inboxIdleClient.Capabilities.HasFlag(ImapCapabilities.Idle))
|
|
||||||
{
|
|
||||||
_logger.Information("Imap server does not support IDLE command. Listening live changes is not supported for {Name}", Account.Name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inboxFolder = _inboxIdleClient.Inbox;
|
|
||||||
|
|
||||||
if (inboxFolder == null)
|
|
||||||
{
|
|
||||||
_logger.Information("Inbox folder is null. Cannot listen for changes.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inboxFolder.CountChanged += InboxFolderCountChanged;
|
|
||||||
inboxFolder.MessageExpunged += InboxFolderMessageExpunged;
|
|
||||||
inboxFolder.MessageFlagsChanged += InboxFolderMessageFlagsChanged;
|
|
||||||
|
|
||||||
while (!cancelInboxListeningToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await AwaitInboxIdleAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
await StopInboxListeningAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InboxFolderMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Flags have changed for message #{0} ({1}).", e.Index, e.Flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InboxFolderMessageExpunged(object sender, MessageEventArgs e)
|
|
||||||
{
|
|
||||||
_logger.Information("Inbox folder message expunged");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InboxFolderCountChanged(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
_logger.Information("Inbox folder count changed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses List of string of mail copy ids and return valid uIds.
|
|
||||||
/// Follow the rules for creating arbitrary unique id for mail copies.
|
|
||||||
/// </summary>
|
|
||||||
private UniqueIdSet GetUniqueIds(IEnumerable<string> mailCopyIds)
|
|
||||||
=> new(mailCopyIds.Select(a => new UniqueId(MailkitClientExtensions.ResolveUid(a))));
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns UniqueId for the given mail copy id.
|
/// Returns UniqueId for the given mail copy id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private UniqueId GetUniqueId(string mailCopyId)
|
private UniqueId GetUniqueId(string mailCopyId) => new(MailkitClientExtensions.ResolveUid(mailCopyId));
|
||||||
=> new(MailkitClientExtensions.ResolveUid(mailCopyId));
|
|
||||||
|
|
||||||
#region Mail Integrations
|
#region Mail Integrations
|
||||||
|
|
||||||
@@ -319,8 +175,6 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
|
|
||||||
var singleRequest = request.Request;
|
var singleRequest = request.Request;
|
||||||
|
|
||||||
singleRequest.Mime.Prepare(EncodingConstraint.None);
|
|
||||||
|
|
||||||
using var smtpClient = new MailKit.Net.Smtp.SmtpClient();
|
using var smtpClient = new MailKit.Net.Smtp.SmtpClient();
|
||||||
|
|
||||||
if (smtpClient.IsConnected && client.IsAuthenticated) return;
|
if (smtpClient.IsConnected && client.IsAuthenticated) return;
|
||||||
@@ -400,22 +254,19 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
|
|
||||||
public override async Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
public override async Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var imapFolder = message.MailFolder;
|
var mailCopy = message.MessageSummary.GetMailDetails(assignedFolder, message.MimeMessage);
|
||||||
var summary = message.MessageSummary;
|
|
||||||
|
|
||||||
var mimeMessage = await imapFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false);
|
|
||||||
var mailCopy = summary.GetMailDetails(assignedFolder, mimeMessage);
|
|
||||||
|
|
||||||
// Draft folder message updates must be updated as IsDraft.
|
// Draft folder message updates must be updated as IsDraft.
|
||||||
// I couldn't find it in MimeMessage...
|
// I couldn't find it in MimeMesssage...
|
||||||
|
|
||||||
mailCopy.IsDraft = assignedFolder.SpecialFolderType == SpecialFolderType.Draft;
|
mailCopy.IsDraft = assignedFolder.SpecialFolderType == SpecialFolderType.Draft;
|
||||||
|
|
||||||
// Check draft mapping.
|
// Check draft mapping.
|
||||||
// This is the same implementation as in the OutlookSynchronizer.
|
// This is the same implementation as in the OutlookSynchronizer.
|
||||||
|
|
||||||
if (mimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader)
|
if (message.MimeMessage != null &&
|
||||||
&& Guid.TryParse(mimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId))
|
message.MimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) &&
|
||||||
|
Guid.TryParse(message.MimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId))
|
||||||
{
|
{
|
||||||
// This message belongs to existing local draft copy.
|
// This message belongs to existing local draft copy.
|
||||||
// We don't need to create a new mail copy for this message, just update the existing one.
|
// We don't need to create a new mail copy for this message, just update the existing one.
|
||||||
@@ -427,7 +278,7 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
// Local copy doesn't exists. Continue execution to insert mail copy.
|
// Local copy doesn't exists. Continue execution to insert mail copy.
|
||||||
}
|
}
|
||||||
|
|
||||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId);
|
var package = new NewMailItemPackage(mailCopy, message.MimeMessage, assignedFolder.RemoteFolderId);
|
||||||
|
|
||||||
return
|
return
|
||||||
[
|
[
|
||||||
@@ -463,7 +314,13 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
PublishSynchronizationProgress(progress);
|
PublishSynchronizationProgress(progress);
|
||||||
|
|
||||||
var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false);
|
var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false);
|
||||||
downloadedMessageIds.AddRange(folderDownloadedMessageIds);
|
|
||||||
|
if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled;
|
||||||
|
|
||||||
|
if (folderDownloadedMessageIds != null)
|
||||||
|
{
|
||||||
|
downloadedMessageIds.AddRange(folderDownloadedMessageIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +354,7 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
// At this point this client is ready to execute async commands.
|
// At this point this client is ready to execute async commands.
|
||||||
// Each task bundle will await and execution will continue in case of error.
|
// Each task bundle will await and execution will continue in case of error.
|
||||||
|
|
||||||
ImapClient executorClient = null;
|
IImapClient executorClient = null;
|
||||||
|
|
||||||
bool isCrashed = false;
|
bool isCrashed = false;
|
||||||
|
|
||||||
@@ -550,7 +407,7 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
/// <param name="executorClient">ImapClient from the pool</param>
|
/// <param name="executorClient">ImapClient from the pool</param>
|
||||||
/// <param name="remoteFolder">Assigning remote folder.</param>
|
/// <param name="remoteFolder">Assigning remote folder.</param>
|
||||||
/// <param name="localFolder">Assigning local folder.</param>
|
/// <param name="localFolder">Assigning local folder.</param>
|
||||||
private void AssignSpecialFolderType(ImapClient executorClient, IMailFolder remoteFolder, MailItemFolder localFolder)
|
private void AssignSpecialFolderType(IImapClient executorClient, IMailFolder remoteFolder, MailItemFolder localFolder)
|
||||||
{
|
{
|
||||||
// Inbox is awlawys available. Don't miss it for assignment even though XList or SpecialUser is not supported.
|
// Inbox is awlawys available. Don't miss it for assignment even though XList or SpecialUser is not supported.
|
||||||
if (executorClient.Inbox == remoteFolder)
|
if (executorClient.Inbox == remoteFolder)
|
||||||
@@ -592,7 +449,7 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
|
|
||||||
var localFolders = await _imapChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
var localFolders = await _imapChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
ImapClient executorClient = null;
|
IImapClient executorClient = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -674,6 +531,11 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists)
|
if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Check for NoSelect folders. These are not selectable folders.
|
||||||
|
// TODO: With new MailKit version 'CanOpen' will be implemented for ease of use. Use that one.
|
||||||
|
if (remoteFolder.Attributes.HasFlag(FolderAttributes.NoSelect))
|
||||||
|
continue;
|
||||||
|
|
||||||
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName);
|
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName);
|
||||||
|
|
||||||
if (existingLocalFolder == null)
|
if (existingLocalFolder == null)
|
||||||
@@ -768,264 +630,42 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<string>> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
|
private async Task<IEnumerable<string>> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!folder.IsSynchronizationEnabled) return default;
|
if (!folder.IsSynchronizationEnabled) return default;
|
||||||
|
|
||||||
var downloadedMessageIds = new List<string>();
|
IImapClient availableClient = null;
|
||||||
|
|
||||||
// STEP1: Ask for flag changes for older mails.
|
|
||||||
// STEP2: Get new mail changes.
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc4549 - Section 4.3
|
|
||||||
|
|
||||||
var _synchronizationClient = await _clientPool.GetClientAsync();
|
|
||||||
|
|
||||||
IMailFolder imapFolder = null;
|
|
||||||
|
|
||||||
var knownMailIds = new UniqueIdSet();
|
|
||||||
var locallyKnownMailUids = await _imapChangeProcessor.GetKnownUidsForFolderAsync(folder.Id);
|
|
||||||
knownMailIds.AddRange(locallyKnownMailUids.Select(a => new UniqueId(a)));
|
|
||||||
|
|
||||||
var highestUniqueId = Math.Max(0, locallyKnownMailUids.Count == 0 ? 0 : locallyKnownMailUids.Max());
|
|
||||||
|
|
||||||
var missingMailIds = new UniqueIdSet();
|
|
||||||
|
|
||||||
var uidValidity = folder.UidValidity;
|
|
||||||
var highestModeSeq = folder.HighestModeSeq;
|
|
||||||
|
|
||||||
var logger = Log.ForContext("FolderName", folder.FolderName);
|
|
||||||
|
|
||||||
logger.Verbose("HighestModeSeq: {HighestModeSeq}, HighestUniqueId: {HighestUniqueId}, UIDValidity: {UIDValidity}", highestModeSeq, highestUniqueId, uidValidity);
|
|
||||||
|
|
||||||
// Event handlers are placed here to handle existing MailItemFolder and IIMailFolder from MailKit.
|
|
||||||
// MailKit doesn't expose folder data when these events are emitted.
|
|
||||||
|
|
||||||
// Use local folder's UidValidty because cache might've been expired for remote IMAP folder.
|
|
||||||
// That will make our mail copy id invalid.
|
|
||||||
|
|
||||||
EventHandler<MessagesVanishedEventArgs> MessageVanishedHandler = async (s, e) =>
|
|
||||||
{
|
|
||||||
if (imapFolder == null) return;
|
|
||||||
|
|
||||||
foreach (var uniqueId in e.UniqueIds)
|
|
||||||
{
|
|
||||||
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, uniqueId.Id);
|
|
||||||
|
|
||||||
await _imapChangeProcessor.DeleteMailAsync(Account.Id, localMailCopyId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EventHandler<MessageFlagsChangedEventArgs> MessageFlagsChangedHandler = async (s, e) =>
|
|
||||||
{
|
|
||||||
if (imapFolder == null) return;
|
|
||||||
if (e.UniqueId == null) return;
|
|
||||||
|
|
||||||
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, e.UniqueId.Value.Id);
|
|
||||||
|
|
||||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(e.Flags);
|
|
||||||
var isRead = MailkitClientExtensions.GetIsRead(e.Flags);
|
|
||||||
|
|
||||||
await _imapChangeProcessor.ChangeMailReadStatusAsync(localMailCopyId, isRead);
|
|
||||||
await _imapChangeProcessor.ChangeFlagStatusAsync(localMailCopyId, isFlagged);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventHandler<MessageEventArgs> MessageExpungedHandler = async (s, e) =>
|
|
||||||
{
|
|
||||||
if (imapFolder == null) return;
|
|
||||||
if (e.UniqueId == null) return;
|
|
||||||
|
|
||||||
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, e.UniqueId.Value.Id);
|
|
||||||
await _imapChangeProcessor.DeleteMailAsync(Account.Id, localMailCopyId);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
retry:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
imapFolder = await _synchronizationClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken);
|
|
||||||
|
|
||||||
imapFolder.MessageFlagsChanged += MessageFlagsChangedHandler;
|
availableClient = await _clientPool.GetClientAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// TODO: Bug: Enabling quick re-sync actually doesn't enable it.
|
var strategy = _imapSynchronizationStrategyProvider.GetSynchronizationStrategy(availableClient);
|
||||||
|
return await strategy.HandleSynchronizationAsync(availableClient, folder, this, cancellationToken).ConfigureAwait(false);
|
||||||
var qsyncEnabled = false; // _synchronizationClient.Capabilities.HasFlag(ImapCapabilities.QuickResync);
|
|
||||||
var condStoreEnabled = _synchronizationClient.Capabilities.HasFlag(ImapCapabilities.CondStore);
|
|
||||||
|
|
||||||
if (qsyncEnabled)
|
|
||||||
{
|
|
||||||
|
|
||||||
imapFolder.MessagesVanished += MessageVanishedHandler;
|
|
||||||
|
|
||||||
await imapFolder.OpenAsync(FolderAccess.ReadWrite, uidValidity, (ulong)highestModeSeq, knownMailIds, cancellationToken);
|
|
||||||
|
|
||||||
// Check the folder validity.
|
|
||||||
// We'll delete our existing cache if it's not.
|
|
||||||
|
|
||||||
// Get all messages after the last successful synchronization date.
|
|
||||||
// This is fine for Wino synchronization because we're not really looking to
|
|
||||||
// synchronize all folder.
|
|
||||||
|
|
||||||
var allMessageIds = await imapFolder.SearchAsync(SearchQuery.All, cancellationToken);
|
|
||||||
|
|
||||||
if (uidValidity != imapFolder.UidValidity)
|
|
||||||
{
|
|
||||||
// TODO: Cache is invalid. Delete all local cache.
|
|
||||||
//await ChangeProcessor.FolderService.ClearImapFolderCacheAsync(folder.Id);
|
|
||||||
|
|
||||||
folder.UidValidity = imapFolder.UidValidity;
|
|
||||||
missingMailIds.AddRange(allMessageIds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Cache is valid.
|
|
||||||
// Add missing mails only.
|
|
||||||
|
|
||||||
missingMailIds.AddRange(allMessageIds.Except(knownMailIds).Where(a => a.Id > highestUniqueId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// QSYNC extension is not enabled for the server.
|
|
||||||
// We rely on ConditionalStore.
|
|
||||||
|
|
||||||
imapFolder.MessageExpunged += MessageExpungedHandler;
|
|
||||||
await imapFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken);
|
|
||||||
|
|
||||||
// Get all messages after the last succesful synchronization date.
|
|
||||||
// This is fine for Wino synchronization because we're not really looking to
|
|
||||||
// synchronize all folder.
|
|
||||||
|
|
||||||
var allMessageIds = await imapFolder.SearchAsync(SearchQuery.All, cancellationToken);
|
|
||||||
|
|
||||||
if (uidValidity != imapFolder.UidValidity)
|
|
||||||
{
|
|
||||||
// TODO: Cache is invalid. Delete all local cache.
|
|
||||||
// await ChangeProcessor.FolderService.ClearImapFolderCacheAsync(folder.Id);
|
|
||||||
|
|
||||||
folder.UidValidity = imapFolder.UidValidity;
|
|
||||||
missingMailIds.AddRange(allMessageIds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Cache is valid.
|
|
||||||
|
|
||||||
var purgedMessages = knownMailIds.Except(allMessageIds);
|
|
||||||
|
|
||||||
foreach (var purgedMessage in purgedMessages)
|
|
||||||
{
|
|
||||||
var mailId = MailkitClientExtensions.CreateUid(folder.Id, purgedMessage.Id);
|
|
||||||
|
|
||||||
await _imapChangeProcessor.DeleteMailAsync(Account.Id, mailId);
|
|
||||||
}
|
|
||||||
|
|
||||||
IList<IMessageSummary> changed;
|
|
||||||
|
|
||||||
if (knownMailIds.Count > 0)
|
|
||||||
{
|
|
||||||
// CONDSTORE enabled. Fetch items with highest mode seq for known items
|
|
||||||
// to track flag changes. Otherwise just get changes without the mode seq.
|
|
||||||
|
|
||||||
if (condStoreEnabled)
|
|
||||||
changed = await imapFolder.FetchAsync(knownMailIds, (ulong)highestModeSeq, MessageSummaryItems.Flags | MessageSummaryItems.ModSeq | MessageSummaryItems.UniqueId);
|
|
||||||
else
|
|
||||||
changed = await imapFolder.FetchAsync(knownMailIds, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId);
|
|
||||||
|
|
||||||
foreach (var changedItem in changed)
|
|
||||||
{
|
|
||||||
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, changedItem.UniqueId.Id);
|
|
||||||
|
|
||||||
var isFlagged = changedItem.Flags.GetIsFlagged();
|
|
||||||
var isRead = changedItem.Flags.GetIsRead();
|
|
||||||
|
|
||||||
await _imapChangeProcessor.ChangeMailReadStatusAsync(localMailCopyId, isRead);
|
|
||||||
await _imapChangeProcessor.ChangeFlagStatusAsync(localMailCopyId, isFlagged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're only interested in items that has highier known uid than we fetched before.
|
|
||||||
// Others are just older messages.
|
|
||||||
|
|
||||||
missingMailIds.AddRange(allMessageIds.Except(knownMailIds).Where(a => a.Id > highestUniqueId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch completely missing new items in the end.
|
|
||||||
|
|
||||||
// Limit check.
|
|
||||||
if (missingMailIds.Count > InitialMessageDownloadCountPerFolder)
|
|
||||||
{
|
|
||||||
missingMailIds = new UniqueIdSet(missingMailIds.TakeLast((int)InitialMessageDownloadCountPerFolder));
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case of the high input, we'll batch them by 50 to reflect changes quickly.
|
|
||||||
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending));
|
|
||||||
|
|
||||||
foreach (var batchMissingMailIds in batchedMissingMailIds)
|
|
||||||
{
|
|
||||||
var summaries = await imapFolder.FetchAsync(batchMissingMailIds, mailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var summary in summaries)
|
|
||||||
{
|
|
||||||
// We pass the opened folder and summary to retrieve raw MimeMessage.
|
|
||||||
|
|
||||||
var creationPackage = new ImapMessageCreationPackage(summary, imapFolder);
|
|
||||||
var createdMailPackages = await CreateNewMailPackagesAsync(creationPackage, folder, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Local draft is mapped. We don't need to create a new mail copy.
|
|
||||||
if (createdMailPackages == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var mailPackage in createdMailPackages)
|
|
||||||
{
|
|
||||||
bool isCreated = await _imapChangeProcessor.CreateMailAsync(Account.Id, mailPackage).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (isCreated)
|
|
||||||
{
|
|
||||||
downloadedMessageIds.Add(mailPackage.Copy.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folder.HighestModeSeq != (long)imapFolder.HighestModSeq)
|
|
||||||
{
|
|
||||||
folder.HighestModeSeq = (long)imapFolder.HighestModSeq;
|
|
||||||
|
|
||||||
await _imapChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last synchronization date for the folder..
|
|
||||||
|
|
||||||
await _imapChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return downloadedMessageIds;
|
|
||||||
}
|
}
|
||||||
catch (FolderNotFoundException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
await _imapChangeProcessor.DeleteFolderAsync(Account.Id, folder.RemoteFolderId).ConfigureAwait(false);
|
_clientPool.Release(availableClient, false);
|
||||||
|
|
||||||
return default;
|
goto retry;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancellations.
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (imapFolder != null)
|
_clientPool.Release(availableClient, false);
|
||||||
{
|
|
||||||
imapFolder.MessageFlagsChanged -= MessageFlagsChangedHandler;
|
|
||||||
imapFolder.MessageExpunged -= MessageExpungedHandler;
|
|
||||||
imapFolder.MessagesVanished -= MessageVanishedHandler;
|
|
||||||
|
|
||||||
if (imapFolder.IsOpen)
|
|
||||||
await imapFolder.CloseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_clientPool.Release(_synchronizationClient);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the local folder should be updated with the remote folder.
|
/// Whether the local folder should be updated with the remote folder.
|
||||||
@@ -1037,8 +677,141 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
=> !localFolder.FolderName.Equals(remoteFolder.Name, StringComparison.OrdinalIgnoreCase);
|
=> !localFolder.FolderName.Equals(remoteFolder.Name, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
protected override Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
protected override Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public async Task StartIdleClientAsync()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
IImapClient idleClient = null;
|
||||||
|
IMailFolder inboxFolder = null;
|
||||||
|
|
||||||
|
bool? reconnect = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await _clientPool.GetClientAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!client.Capabilities.HasFlag(ImapCapabilities.Idle))
|
||||||
|
{
|
||||||
|
Log.Debug($"{Account.Name} does not support Idle command. Ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.Inbox == null)
|
||||||
|
{
|
||||||
|
Log.Warning($"{Account.Name} does not have an Inbox folder for idle client to track. Ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup idle client.
|
||||||
|
idleClient = client;
|
||||||
|
|
||||||
|
idleDoneTokenSource ??= new CancellationTokenSource();
|
||||||
|
idleCancellationTokenSource ??= new CancellationTokenSource();
|
||||||
|
|
||||||
|
inboxFolder = client.Inbox;
|
||||||
|
|
||||||
|
await inboxFolder.OpenAsync(FolderAccess.ReadOnly, idleCancellationTokenSource.Token);
|
||||||
|
|
||||||
|
inboxFolder.CountChanged += IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessageFlagsChanged += IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessageExpunged += IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessagesVanished += IdleNotificationTriggered;
|
||||||
|
|
||||||
|
Log.Debug("Starting an idle client for {Name}", Account.Name);
|
||||||
|
|
||||||
|
await client.IdleAsync(idleDoneTokenSource.Token, idleCancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (ImapProtocolException protocolException)
|
||||||
|
{
|
||||||
|
Log.Warning(protocolException, "Idle client received protocol exception.");
|
||||||
|
reconnect = true;
|
||||||
|
}
|
||||||
|
catch (IOException ioException)
|
||||||
|
{
|
||||||
|
Log.Warning(ioException, "Idle client received IO exception.");
|
||||||
|
reconnect = true;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
reconnect = !IsDisposing;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Idle client failed to start.");
|
||||||
|
reconnect = false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (inboxFolder != null)
|
||||||
|
{
|
||||||
|
inboxFolder.CountChanged -= IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessageFlagsChanged -= IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessageExpunged -= IdleNotificationTriggered;
|
||||||
|
inboxFolder.MessagesVanished -= IdleNotificationTriggered;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idleDoneTokenSource != null)
|
||||||
|
{
|
||||||
|
idleDoneTokenSource.Dispose();
|
||||||
|
idleDoneTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idleClient != null)
|
||||||
|
{
|
||||||
|
// Killing the client is not necessary. We can re-use it later.
|
||||||
|
_clientPool.Release(idleClient, destroyClient: false);
|
||||||
|
|
||||||
|
idleClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reconnect == true)
|
||||||
|
{
|
||||||
|
Log.Information("Idle client is reconnecting.");
|
||||||
|
|
||||||
|
_ = StartIdleClientAsync();
|
||||||
|
}
|
||||||
|
else if (reconnect == false)
|
||||||
|
{
|
||||||
|
Log.Information("Finalized idle client.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequestIdleChangeSynchronization()
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Detected idle change.");
|
||||||
|
|
||||||
|
// We don't really need to act on the count change in detail.
|
||||||
|
// Our synchronization should be enough to handle the changes with on-demand sync.
|
||||||
|
// We can just trigger a sync here IMAPIdle type.
|
||||||
|
|
||||||
|
var options = new MailSynchronizationOptions()
|
||||||
|
{
|
||||||
|
AccountId = Account.Id,
|
||||||
|
Type = MailSynchronizationType.IMAPIdle
|
||||||
|
};
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IdleNotificationTriggered(object sender, EventArgs e)
|
||||||
|
=> RequestIdleChangeSynchronization();
|
||||||
|
|
||||||
|
public Task StopIdleClientAsync()
|
||||||
|
{
|
||||||
|
idleDoneTokenSource?.Cancel();
|
||||||
|
idleCancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task KillSynchronizerAsync()
|
||||||
|
{
|
||||||
|
await base.KillSynchronizerAsync();
|
||||||
|
await StopIdleClientAsync();
|
||||||
|
|
||||||
|
// Make sure the client pool safely disconnects all ImapClients.
|
||||||
|
_clientPool.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1165,5 +1165,12 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
|
|
||||||
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task KillSynchronizerAsync()
|
||||||
|
{
|
||||||
|
await base.KillSynchronizerAsync();
|
||||||
|
|
||||||
|
_graphClient.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ namespace Wino.Core.Synchronizers
|
|||||||
{
|
{
|
||||||
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
|
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
|
||||||
{
|
{
|
||||||
|
protected bool IsDisposing { get; private set; }
|
||||||
|
|
||||||
|
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
|
||||||
|
|
||||||
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
|
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
|
||||||
|
|
||||||
protected WinoSynchronizer(MailAccount account) : base(account)
|
protected WinoSynchronizer(MailAccount account) : base(account) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many items per single HTTP call can be modified.
|
/// How many items per single HTTP call can be modified.
|
||||||
@@ -83,94 +85,129 @@ namespace Wino.Core.Synchronizers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
activeSynchronizationCancellationToken = cancellationToken;
|
if (!ShouldQueueMailSynchronization(options))
|
||||||
|
|
||||||
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;
|
Log.Debug($"{options.Type} synchronization is ignored.");
|
||||||
|
return MailSynchronizationResult.Canceled;
|
||||||
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();
|
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
|
||||||
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
|
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
|
||||||
|
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||||
|
|
||||||
PublishSynchronizationProgress(1);
|
PublishSynchronizationProgress(1);
|
||||||
|
|
||||||
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken);
|
// 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;
|
||||||
|
|
||||||
PublishUnreadItemChanges();
|
bool shouldDelayExecution = false;
|
||||||
|
int maxExecutionDelay = 0;
|
||||||
|
|
||||||
// Execute request sync options should be re-calculated after execution.
|
if (shouldExecuteRequests && changeRequestQueue.Any())
|
||||||
// This is the part we decide which individual folders must be synchronized
|
{
|
||||||
// after the batch request execution.
|
State = AccountSynchronizerState.ExecutingRequests;
|
||||||
if (options.Type == MailSynchronizationType.ExecuteRequests)
|
|
||||||
options = GetSynchronizationOptionsAfterRequestExecution(requestCopies);
|
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;
|
State = AccountSynchronizerState.Synchronizing;
|
||||||
|
|
||||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
|
||||||
|
|
||||||
// Handle special synchronization types.
|
// Handle special synchronization types.
|
||||||
|
|
||||||
// Profile information sync.
|
// Profile information sync.
|
||||||
@@ -213,19 +250,9 @@ namespace Wino.Core.Synchronizers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 =
|
|
||||||
(Account.ProviderType == MailProviderType.Outlook || Account.ProviderType == MailProviderType.Office365)
|
|
||||||
&& requestCopies.Any(a => a.ResynchronizationDelay > 0);
|
|
||||||
|
|
||||||
if (shouldDelayExecution)
|
if (shouldDelayExecution)
|
||||||
{
|
{
|
||||||
var maxDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
|
await Task.Delay(maxExecutionDelay);
|
||||||
|
|
||||||
await Task.Delay(maxDelay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the internal synchronization.
|
// Start the internal synchronization.
|
||||||
@@ -249,6 +276,15 @@ namespace Wino.Core.Synchronizers
|
|||||||
}
|
}
|
||||||
finally
|
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)
|
||||||
|
{
|
||||||
|
PendingSynchronizationRequest.Remove(pendingRequest.Key);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset account progress to hide the progress.
|
// Reset account progress to hide the progress.
|
||||||
PublishSynchronizationProgress(0);
|
PublishSynchronizationProgress(0);
|
||||||
|
|
||||||
@@ -288,7 +324,7 @@ namespace Wino.Core.Synchronizers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="batches">Batch requests to run in synchronization.</param>
|
/// <param name="batches">Batch requests to run in synchronization.</param>
|
||||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||||
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests)
|
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
||||||
{
|
{
|
||||||
List<Guid> synchronizationFolderIds = requests
|
List<Guid> synchronizationFolderIds = requests
|
||||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||||
@@ -301,6 +337,8 @@ namespace Wino.Core.Synchronizers
|
|||||||
AccountId = Account.Id,
|
AccountId = Account.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
options.Id = existingSynchronizationId;
|
||||||
|
|
||||||
if (synchronizationFolderIds.Count > 0)
|
if (synchronizationFolderIds.Count > 0)
|
||||||
{
|
{
|
||||||
// Gather FolderIds to synchronize.
|
// Gather FolderIds to synchronize.
|
||||||
@@ -317,6 +355,35 @@ namespace Wino.Core.Synchronizers
|
|||||||
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))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
#region Mail/Folder Operations
|
#region Mail/Folder Operations
|
||||||
|
|
||||||
public virtual bool DelaySendOperationSynchronization() => false;
|
public virtual bool DelaySendOperationSynchronization() => false;
|
||||||
@@ -349,12 +416,12 @@ namespace Wino.Core.Synchronizers
|
|||||||
/// <param name="cancellationToken">Cancellation token.</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 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<ImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
|
public List<IRequestBundle<ImapRequest>> CreateSingleTaskBundle(Func<IImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
|
||||||
{
|
{
|
||||||
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
|
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<ImapClient, TSingeRequestType, Task> value,
|
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<IImapClient, TSingeRequestType, Task> value,
|
||||||
List<TSingeRequestType> requests)
|
List<TSingeRequestType> requests)
|
||||||
where TSingeRequestType : IRequestBase, IUIChangeRequest
|
where TSingeRequestType : IRequestBase, IUIChangeRequest
|
||||||
{
|
{
|
||||||
@@ -368,6 +435,21 @@ namespace Wino.Core.Synchronizers
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual Task KillSynchronizerAsync()
|
||||||
|
{
|
||||||
|
IsDisposing = true;
|
||||||
|
CancelAllSynchronizations();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CancelAllSynchronizations()
|
||||||
|
{
|
||||||
|
foreach (var request in PendingSynchronizationRequest)
|
||||||
|
{
|
||||||
|
request.Value.Cancel();
|
||||||
|
request.Value.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,32 +7,34 @@
|
|||||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Diagnostics" />
|
<PackageReference Include="CommunityToolkit.Diagnostics" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||||
<PackageReference Include="Google.Apis.Calendar.v3" />
|
<PackageReference Include="Google.Apis.Calendar.v3" />
|
||||||
<PackageReference Include="Google.Apis.Gmail.v1" />
|
<PackageReference Include="Google.Apis.Gmail.v1" />
|
||||||
<PackageReference Include="Google.Apis.PeopleService.v1" />
|
<PackageReference Include="Google.Apis.PeopleService.v1" />
|
||||||
<PackageReference Include="HtmlAgilityPack" />
|
<PackageReference Include="HtmlAgilityPack" />
|
||||||
<PackageReference Include="HtmlKit" />
|
<PackageReference Include="HtmlKit" />
|
||||||
<PackageReference Include="MailKit" />
|
<PackageReference Include="MailKit" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Graph" />
|
<PackageReference Include="Microsoft.Graph" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" />
|
<PackageReference Include="Microsoft.Identity.Client" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client.Broker" />
|
<PackageReference Include="Microsoft.Identity.Client.Broker" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
|
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
|
||||||
<PackageReference Include="MimeKit" />
|
<PackageReference Include="MimeKit" />
|
||||||
<PackageReference Include="morelinq" />
|
<PackageReference Include="morelinq" />
|
||||||
<PackageReference Include="Nito.AsyncEx.Tasks" />
|
<PackageReference Include="Nito.AsyncEx.Tasks" />
|
||||||
<PackageReference Include="NodaTime" />
|
<PackageReference Include="NodaTime" />
|
||||||
<PackageReference Include="Serilog" />
|
<PackageReference Include="Serilog" />
|
||||||
<PackageReference Include="Serilog.Exceptions" />
|
<PackageReference Include="Serilog.Exceptions" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" />
|
<PackageReference Include="Serilog.Sinks.Debug" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" />
|
<PackageReference Include="Serilog.Sinks.File" />
|
||||||
<PackageReference Include="SkiaSharp" />
|
<PackageReference Include="SkiaSharp" />
|
||||||
<PackageReference Include="SqlKata" />
|
<PackageReference Include="SqlKata" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels
|
namespace Wino.Mail.ViewModels
|
||||||
@@ -20,6 +21,7 @@ namespace Wino.Mail.ViewModels
|
|||||||
{
|
{
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly IWinoServerConnectionManager _serverConnectionManager;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
|
|
||||||
public MailAccount Account { get; set; }
|
public MailAccount Account { get; set; }
|
||||||
@@ -48,10 +50,12 @@ namespace Wino.Mail.ViewModels
|
|||||||
|
|
||||||
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
|
IWinoServerConnectionManager serverConnectionManager,
|
||||||
IFolderService folderService)
|
IFolderService folderService)
|
||||||
{
|
{
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
|
_serverConnectionManager = serverConnectionManager;
|
||||||
_folderService = folderService;
|
_folderService = folderService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +106,17 @@ namespace Wino.Mail.ViewModels
|
|||||||
if (!confirmation)
|
if (!confirmation)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await _accountService.DeleteAccountAsync(Account);
|
|
||||||
|
|
||||||
// TODO: Server: Cancel ongoing calls from server for this account.
|
var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync<bool, KillAccountSynchronizerRequested>(new KillAccountSynchronizerRequested(Account.Id));
|
||||||
|
|
||||||
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
|
if (isSynchronizerKilledResponse.IsSuccess)
|
||||||
|
{
|
||||||
|
await _accountService.DeleteAccountAsync(Account);
|
||||||
|
|
||||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
|
||||||
|
|
||||||
|
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
|
|||||||
@@ -28,18 +28,25 @@ namespace Wino.Mail.ViewModels
|
|||||||
{
|
{
|
||||||
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
|
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
|
||||||
{
|
{
|
||||||
|
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
|
||||||
|
private readonly IImapTestService _imapTestService;
|
||||||
|
|
||||||
public IMailDialogService MailDialogService { get; }
|
public IMailDialogService MailDialogService { get; }
|
||||||
|
|
||||||
public AccountManagementViewModel(IMailDialogService dialogService,
|
public AccountManagementViewModel(IMailDialogService dialogService,
|
||||||
IWinoServerConnectionManager winoServerConnectionManager,
|
IWinoServerConnectionManager winoServerConnectionManager,
|
||||||
INavigationService navigationService,
|
INavigationService navigationService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
|
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
|
IImapTestService imapTestService,
|
||||||
IStoreManagementService storeManagementService,
|
IStoreManagementService storeManagementService,
|
||||||
IAuthenticationProvider authenticationProvider,
|
IAuthenticationProvider authenticationProvider,
|
||||||
IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
|
IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
|
||||||
{
|
{
|
||||||
MailDialogService = dialogService;
|
MailDialogService = dialogService;
|
||||||
|
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
|
||||||
|
_imapTestService = imapTestService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -93,7 +100,7 @@ namespace Wino.Mail.ViewModels
|
|||||||
|
|
||||||
if (accountCreationDialogResult != null)
|
if (accountCreationDialogResult != null)
|
||||||
{
|
{
|
||||||
creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
|
creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
|
||||||
|
|
||||||
CustomServerInformation customServerInformation = null;
|
CustomServerInformation customServerInformation = null;
|
||||||
|
|
||||||
@@ -101,17 +108,17 @@ namespace Wino.Mail.ViewModels
|
|||||||
{
|
{
|
||||||
ProviderType = accountCreationDialogResult.ProviderType,
|
ProviderType = accountCreationDialogResult.ProviderType,
|
||||||
Name = accountCreationDialogResult.AccountName,
|
Name = accountCreationDialogResult.AccountName,
|
||||||
AccountColorHex = accountCreationDialogResult.AccountColorHex,
|
SpecialImapProvider = accountCreationDialogResult.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None,
|
||||||
Id = Guid.NewGuid()
|
Id = Guid.NewGuid()
|
||||||
};
|
};
|
||||||
|
|
||||||
creationDialog.ShowDialog(accountCreationCancellationTokenSource);
|
await creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource);
|
||||||
creationDialog.State = AccountCreationDialogState.SigningIn;
|
creationDialog.State = AccountCreationDialogState.SigningIn;
|
||||||
|
|
||||||
string tokenInformation = string.Empty;
|
string tokenInformation = string.Empty;
|
||||||
|
|
||||||
// Custom server implementation requires more async waiting.
|
// Custom server implementation requires more async waiting.
|
||||||
if (creationDialog is ICustomServerAccountCreationDialog customServerDialog)
|
if (creationDialog is IImapAccountCreationDialog customServerDialog)
|
||||||
{
|
{
|
||||||
// Pass along the account properties and perform initial navigation on the imap frame.
|
// Pass along the account properties and perform initial navigation on the imap frame.
|
||||||
customServerDialog.StartImapConnectionSetup(createdAccount);
|
customServerDialog.StartImapConnectionSetup(createdAccount);
|
||||||
@@ -130,20 +137,39 @@ namespace Wino.Mail.ViewModels
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// OAuth authentication is handled here.
|
// Hanle special imap providers like iCloud and Yahoo.
|
||||||
// Server authenticates, returns the token info here.
|
if (accountCreationDialogResult.SpecialImapProviderDetails != null)
|
||||||
|
{
|
||||||
|
// Special imap provider testing dialog. This is only available for iCloud and Yahoo.
|
||||||
|
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult);
|
||||||
|
customServerInformation.Id = Guid.NewGuid();
|
||||||
|
customServerInformation.AccountId = createdAccount.Id;
|
||||||
|
|
||||||
var tokenInformationResponse = await WinoServerConnectionManager
|
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
|
||||||
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
|
createdAccount.Address = customServerInformation.Address;
|
||||||
createdAccount,
|
|
||||||
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
|
|
||||||
|
|
||||||
if (creationDialog.State == AccountCreationDialogState.Canceled)
|
await _imapTestService.TestImapConnectionAsync(customServerInformation, true);
|
||||||
throw new AccountSetupCanceledException();
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// OAuth authentication is handled here.
|
||||||
|
// Server authenticates, returns the token info here.
|
||||||
|
|
||||||
createdAccount.Address = tokenInformationResponse.Data.AccountAddress;
|
var tokenInformationResponse = await WinoServerConnectionManager
|
||||||
|
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
|
||||||
|
createdAccount,
|
||||||
|
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
|
||||||
|
|
||||||
tokenInformationResponse.ThrowIfFailed();
|
if (creationDialog.State == AccountCreationDialogState.Canceled)
|
||||||
|
throw new AccountSetupCanceledException();
|
||||||
|
|
||||||
|
if (!tokenInformationResponse.IsSuccess)
|
||||||
|
throw new Exception(tokenInformationResponse.Message);
|
||||||
|
|
||||||
|
createdAccount.Address = tokenInformationResponse.Data.AccountAddress;
|
||||||
|
|
||||||
|
tokenInformationResponse.ThrowIfFailed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address is still doesn't have a value for API synchronizers.
|
// Address is still doesn't have a value for API synchronizers.
|
||||||
@@ -183,7 +209,7 @@ namespace Wino.Mail.ViewModels
|
|||||||
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
|
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog)
|
if (creationDialog is IImapAccountCreationDialog customServerAccountCreationDialog)
|
||||||
customServerAccountCreationDialog.ShowPreparingFolders();
|
customServerAccountCreationDialog.ShowPreparingFolders();
|
||||||
else
|
else
|
||||||
creationDialog.State = AccountCreationDialogState.PreparingFolders;
|
creationDialog.State = AccountCreationDialogState.PreparingFolders;
|
||||||
@@ -227,14 +253,6 @@ namespace Wino.Mail.ViewModels
|
|||||||
await AccountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
|
await AccountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Temporary disabled. Is this even needed? Users can configure special folders manually later on if discovery fails.
|
|
||||||
// Check if Inbox folder is available for the account after synchronization.
|
|
||||||
|
|
||||||
//var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
|
|
||||||
|
|
||||||
//if (!isInboxAvailable)
|
|
||||||
// throw new Exception(Translator.Exception_InboxNotAvailable);
|
|
||||||
|
|
||||||
// Send changes to listeners.
|
// Send changes to listeners.
|
||||||
ReportUIChange(new AccountCreatedMessage(createdAccount));
|
ReportUIChange(new AccountCreatedMessage(createdAccount));
|
||||||
|
|
||||||
@@ -250,6 +268,10 @@ namespace Wino.Mail.ViewModels
|
|||||||
{
|
{
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
|
catch (ImapClientPoolException clientPoolException)
|
||||||
|
{
|
||||||
|
DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, clientPoolException.InnerException.Message, InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, WinoErrors.AccountCreation);
|
Log.Error(ex, WinoErrors.AccountCreation);
|
||||||
|
|||||||
@@ -243,7 +243,9 @@ namespace Wino.Mail.ViewModels
|
|||||||
await RecreateMenuItemsAsync();
|
await RecreateMenuItemsAsync();
|
||||||
await ProcessLaunchOptionsAsync();
|
await ProcessLaunchOptionsAsync();
|
||||||
|
|
||||||
|
#if !DEBUG
|
||||||
await ForceAllAccountSynchronizationsAsync();
|
await ForceAllAccountSynchronizationsAsync();
|
||||||
|
#endif
|
||||||
await MakeSureEnableStartupLaunchAsync();
|
await MakeSureEnableStartupLaunchAsync();
|
||||||
await ConfigureBackgroundTasksAsync();
|
await ConfigureBackgroundTasksAsync();
|
||||||
}
|
}
|
||||||
@@ -878,6 +880,8 @@ namespace Wino.Mail.ViewModels
|
|||||||
|
|
||||||
protected override async void OnAccountCreated(MailAccount createdAccount)
|
protected override async void OnAccountCreated(MailAccount createdAccount)
|
||||||
{
|
{
|
||||||
|
latestSelectedAccountMenuItem = null;
|
||||||
|
|
||||||
await RecreateMenuItemsAsync();
|
await RecreateMenuItemsAsync();
|
||||||
|
|
||||||
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return;
|
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return;
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ namespace Wino
|
|||||||
}
|
}
|
||||||
catch (WinoServerException serverException)
|
catch (WinoServerException serverException)
|
||||||
{
|
{
|
||||||
|
// TODO: Exception context is lost.
|
||||||
var dialogService = Services.GetService<IMailDialogService>();
|
var dialogService = Services.GetService<IMailDialogService>();
|
||||||
|
|
||||||
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
|
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
|
||||||
@@ -241,6 +242,8 @@ namespace Wino
|
|||||||
|
|
||||||
protected override async void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
|
protected override async void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
|
||||||
{
|
{
|
||||||
|
Log.Information("App close requested.");
|
||||||
|
|
||||||
var deferral = e.GetDeferral();
|
var deferral = e.GetDeferral();
|
||||||
|
|
||||||
// Wino should notify user on app close if:
|
// Wino should notify user on app close if:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</TransitionCollection>
|
</TransitionCollection>
|
||||||
</coreControls:WinoNavigationViewItem.ContentTransitions>
|
</coreControls:WinoNavigationViewItem.ContentTransitions>
|
||||||
<muxc:NavigationViewItem.Icon>
|
<muxc:NavigationViewItem.Icon>
|
||||||
<coreControls:WinoFontIcon FontSize="12" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Parameter.ProviderType)}" />
|
<coreControls:WinoFontIcon FontSize="12" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Parameter)}" />
|
||||||
</muxc:NavigationViewItem.Icon>
|
</muxc:NavigationViewItem.Icon>
|
||||||
<muxc:NavigationViewItem.InfoBadge>
|
<muxc:NavigationViewItem.InfoBadge>
|
||||||
<muxc:InfoBadge
|
<muxc:InfoBadge
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="24"
|
FontSize="24"
|
||||||
Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(ProviderDetail.Type)}" />
|
Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(ProviderDetail.Type, ProviderDetail.SpecialImapProvider)}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Windows.UI.Xaml.Media.Animation;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Messaging.Client.Mails;
|
using Wino.Messaging.Client.Mails;
|
||||||
using Wino.Views.ImapSetup;
|
using Wino.Views.ImapSetup;
|
||||||
|
|
||||||
@@ -24,10 +25,10 @@ namespace Wino.Dialogs
|
|||||||
IRecipient<ImapSetupNavigationRequested>,
|
IRecipient<ImapSetupNavigationRequested>,
|
||||||
IRecipient<ImapSetupBackNavigationRequested>,
|
IRecipient<ImapSetupBackNavigationRequested>,
|
||||||
IRecipient<ImapSetupDismissRequested>,
|
IRecipient<ImapSetupDismissRequested>,
|
||||||
ICustomServerAccountCreationDialog
|
IImapAccountCreationDialog
|
||||||
{
|
{
|
||||||
private TaskCompletionSource<CustomServerInformation> _getServerInfoTaskCompletionSource = new TaskCompletionSource<CustomServerInformation>();
|
private TaskCompletionSource<CustomServerInformation> _getServerInfoTaskCompletionSource = new TaskCompletionSource<CustomServerInformation>();
|
||||||
|
private TaskCompletionSource<bool> dialogOpened = new TaskCompletionSource<bool>();
|
||||||
private bool isDismissRequested = false;
|
private bool isDismissRequested = false;
|
||||||
|
|
||||||
public NewImapSetupDialog()
|
public NewImapSetupDialog()
|
||||||
@@ -77,8 +78,21 @@ namespace Wino.Dialogs
|
|||||||
|
|
||||||
public void Receive(ImapSetupDismissRequested message) => _getServerInfoTaskCompletionSource.TrySetResult(message.CompletedServerInformation);
|
public void Receive(ImapSetupDismissRequested message) => _getServerInfoTaskCompletionSource.TrySetResult(message.CompletedServerInformation);
|
||||||
|
|
||||||
public void ShowDialog(CancellationTokenSource cancellationTokenSource)
|
public async Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource)
|
||||||
=> _ = ShowAsync();
|
{
|
||||||
|
Opened += DialogOpened;
|
||||||
|
|
||||||
|
_ = ShowAsync();
|
||||||
|
|
||||||
|
await dialogOpened.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args)
|
||||||
|
{
|
||||||
|
Opened -= DialogOpened;
|
||||||
|
|
||||||
|
dialogOpened?.SetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowPreparingFolders()
|
public void ShowPreparingFolders()
|
||||||
{
|
{
|
||||||
@@ -86,7 +100,7 @@ namespace Wino.Dialogs
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void StartImapConnectionSetup(MailAccount account) => ImapFrame.Navigate(typeof(WelcomeImapSetupPage), account, new DrillInNavigationTransitionInfo());
|
public void StartImapConnectionSetup(MailAccount account) => ImapFrame.Navigate(typeof(WelcomeImapSetupPage), account, new DrillInNavigationTransitionInfo());
|
||||||
|
public void StartImapConnectionSetup(AccountCreationDialogResult accountCreationDialogResult) => ImapFrame.Navigate(typeof(WelcomeImapSetupPage), accountCreationDialogResult, new DrillInNavigationTransitionInfo());
|
||||||
|
|
||||||
private void ImapSetupDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) => WeakReferenceMessenger.Default.UnregisterAll(this);
|
private void ImapSetupDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) => WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.UI.Xaml.Controls;
|
using Windows.UI.Xaml.Controls;
|
||||||
using Wino.Controls;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.UWP.Controls;
|
using Wino.Core.UWP.Controls;
|
||||||
using Wino.Helpers;
|
using Wino.Helpers;
|
||||||
@@ -22,7 +21,7 @@ namespace Wino.MenuFlyouts
|
|||||||
|
|
||||||
foreach (var account in _accounts)
|
foreach (var account in _accounts)
|
||||||
{
|
{
|
||||||
var pathData = new WinoFontIcon() { Icon = XamlHelpers.GetProviderIcon(account.ProviderType) };
|
var pathData = new WinoFontIcon() { Icon = XamlHelpers.GetProviderIcon(account) };
|
||||||
var menuItem = new MenuFlyoutItem() { Tag = account.Address, Icon = pathData, Text = $"{account.Name} ({account.Address})", MinHeight = 55 };
|
var menuItem = new MenuFlyoutItem() { Tag = account.Address, Icon = pathData, Text = $"{account.Name} ({account.Address})", MinHeight = 55 };
|
||||||
|
|
||||||
menuItem.Click += AccountClicked;
|
menuItem.Click += AccountClicked;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Wino.Core.Domain.Entities.Mail;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.UWP.Extensions;
|
using Wino.Core.UWP.Extensions;
|
||||||
@@ -30,18 +31,27 @@ namespace Wino.Services
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IAccountCreationDialog GetAccountCreationDialog(MailProviderType type)
|
public override IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult)
|
||||||
{
|
{
|
||||||
if (type == MailProviderType.IMAP4)
|
if (accountCreationDialogResult.SpecialImapProviderDetails == null)
|
||||||
{
|
{
|
||||||
return new NewImapSetupDialog
|
if (accountCreationDialogResult.ProviderType == MailProviderType.IMAP4)
|
||||||
{
|
{
|
||||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
return new NewImapSetupDialog
|
||||||
};
|
{
|
||||||
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return base.GetAccountCreationDialog(accountCreationDialogResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return base.GetAccountCreationDialog(type);
|
// Special IMAP provider like iCloud or Yahoo.
|
||||||
|
|
||||||
|
return base.GetAccountCreationDialog(accountCreationDialogResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,20 +20,15 @@ namespace Wino.Mail.Services
|
|||||||
|
|
||||||
public List<IProviderDetail> GetAvailableProviders()
|
public List<IProviderDetail> GetAvailableProviders()
|
||||||
{
|
{
|
||||||
var providerList = new List<IProviderDetail>();
|
var providerList = new List<IProviderDetail>
|
||||||
|
|
||||||
var providers = new MailProviderType[]
|
|
||||||
{
|
{
|
||||||
MailProviderType.Outlook,
|
new ProviderDetail(MailProviderType.Outlook, SpecialImapProvider.None),
|
||||||
MailProviderType.Gmail,
|
new ProviderDetail(MailProviderType.Gmail, SpecialImapProvider.None),
|
||||||
MailProviderType.IMAP4
|
new ProviderDetail(MailProviderType.IMAP4, SpecialImapProvider.iCloud),
|
||||||
|
new ProviderDetail(MailProviderType.IMAP4, SpecialImapProvider.Yahoo),
|
||||||
|
new ProviderDetail(MailProviderType.IMAP4, SpecialImapProvider.None)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var type in providers)
|
|
||||||
{
|
|
||||||
providerList.Add(new ProviderDetail(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
return providerList;
|
return providerList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
Header="{x:Bind Account.Name}"
|
Header="{x:Bind Account.Name}"
|
||||||
IsClickEnabled="True">
|
IsClickEnabled="True">
|
||||||
<controls:SettingsCard.HeaderIcon>
|
<controls:SettingsCard.HeaderIcon>
|
||||||
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(ProviderDetail.Type)}" />
|
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Account)}" />
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
<controls:SettingsCard.ActionIcon>
|
<controls:SettingsCard.ActionIcon>
|
||||||
<PathIcon
|
<PathIcon
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
Header="{x:Bind Account.Name}"
|
Header="{x:Bind Account.Name}"
|
||||||
IsClickEnabled="True">
|
IsClickEnabled="True">
|
||||||
<controls:SettingsCard.HeaderIcon>
|
<controls:SettingsCard.HeaderIcon>
|
||||||
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(ProviderDetail.Type)}" />
|
<coreControls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Account)}" />
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
|
|
||||||
<controls:SettingsCard.ActionIcon>
|
<controls:SettingsCard.ActionIcon>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Wino.Core.Domain;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Exceptions;
|
using Wino.Core.Domain.Exceptions;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.AutoDiscovery;
|
using Wino.Core.Domain.Models.AutoDiscovery;
|
||||||
using Wino.Messaging.Client.Mails;
|
using Wino.Messaging.Client.Mails;
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ namespace Wino.Views.ImapSetup
|
|||||||
{
|
{
|
||||||
DisplayNameBox.Text = accountProperties.Name;
|
DisplayNameBox.Text = accountProperties.Name;
|
||||||
}
|
}
|
||||||
|
else if (e.Parameter is AccountCreationDialogResult creationDialogResult)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new ImapSetupNavigationRequested(typeof(TestingImapConnectionPage), creationDialogResult));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SignInClicked(object sender, RoutedEventArgs e)
|
private async void SignInClicked(object sender, RoutedEventArgs e)
|
||||||
|
|||||||
@@ -422,6 +422,7 @@
|
|||||||
<!-- Mail Items -->
|
<!-- Mail Items -->
|
||||||
<muxc:RefreshContainer
|
<muxc:RefreshContainer
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
Margin="-4,0"
|
||||||
RefreshRequested="PullToRefreshRequested"
|
RefreshRequested="PullToRefreshRequested"
|
||||||
Visibility="{x:Bind ViewModel.IsEmpty, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}">
|
Visibility="{x:Bind ViewModel.IsEmpty, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}">
|
||||||
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
|
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
|
||||||
@@ -444,7 +445,7 @@
|
|||||||
</ListView.ItemContainerTransitions>
|
</ListView.ItemContainerTransitions>
|
||||||
<ListView.ItemsPanel>
|
<ListView.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" />
|
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ListView.ItemsPanel>
|
</ListView.ItemsPanel>
|
||||||
<ListView.Resources>
|
<ListView.Resources>
|
||||||
@@ -466,13 +467,13 @@
|
|||||||
<Style TargetType="ListViewItem">
|
<Style TargetType="ListViewItem">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||||
<Setter Property="Margin" Value="0,12" />
|
<Setter Property="Margin" Value="12" />
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
</Style>
|
</Style>
|
||||||
</ListView.Resources>
|
</ListView.Resources>
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="ICollectionViewGroup">
|
<DataTemplate x:DataType="ICollectionViewGroup">
|
||||||
<Grid Background="{ThemeResource MailListHeaderBackgroundColor}" CornerRadius="4">
|
<Grid Background="{ThemeResource MailListHeaderBackgroundColor}" CornerRadius="6">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="12,0"
|
Margin="12,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|||||||
11
Wino.Messages/Server/KillAccountSynchronizerRequested.cs
Normal file
11
Wino.Messages/Server/KillAccountSynchronizerRequested.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace Wino.Messaging.Server
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client message that requests to kill the account synchronizer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AccountId">Account id to kill synchronizer for.</param>
|
||||||
|
public record KillAccountSynchronizerRequested(Guid AccountId) : IClientMessage;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace Wino.Messaging.Server
|
|||||||
/// Triggers a new mail synchronization if possible.
|
/// Triggers a new mail synchronization if possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Options">Options for synchronization.</param>
|
/// <param name="Options">Options for synchronization.</param>
|
||||||
public record NewMailSynchronizationRequested(MailSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage;
|
public record NewMailSynchronizationRequested(MailSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage, IUIMessage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggers a new calendar synchronization if possible.
|
/// Triggers a new calendar synchronization if possible.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="58272BurakKSE.WinoMailPreview"
|
Name="58272BurakKSE.WinoMailPreview"
|
||||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||||
Version="1.9.43.0" />
|
Version="1.9.50.0" />
|
||||||
|
|
||||||
<Extensions>
|
<Extensions>
|
||||||
<!-- Publisher Cache Folders -->
|
<!-- Publisher Cache Folders -->
|
||||||
|
|||||||
@@ -48,12 +48,13 @@
|
|||||||
<EntryPointProjectUniqueName>..\Wino.Mail\Wino.Mail.csproj</EntryPointProjectUniqueName>
|
<EntryPointProjectUniqueName>..\Wino.Mail\Wino.Mail.csproj</EntryPointProjectUniqueName>
|
||||||
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
|
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
|
||||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||||
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
|
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
|
||||||
<AppxPackageDir>$(USERPROFILE)\Desktop\Packages\</AppxPackageDir>
|
<AppxPackageDir>$(USERPROFILE)\Desktop\Packages\</AppxPackageDir>
|
||||||
<GenerateTestArtifacts>True</GenerateTestArtifacts>
|
<GenerateTestArtifacts>True</GenerateTestArtifacts>
|
||||||
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
|
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
|
||||||
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
|
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
|
||||||
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
||||||
|
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<AppxBundle>Always</AppxBundle>
|
<AppxBundle>Always</AppxBundle>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using H.NotifyIcon;
|
using H.NotifyIcon;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
using Wino.Calendar.Services;
|
using Wino.Calendar.Services;
|
||||||
using Wino.Core;
|
using Wino.Core;
|
||||||
@@ -188,6 +189,9 @@ namespace Wino.Server
|
|||||||
|
|
||||||
if (isCreatedNew)
|
if (isCreatedNew)
|
||||||
{
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += ServerCrashed;
|
||||||
|
Application.Current.DispatcherUnhandledException += UIThreadCrash;
|
||||||
|
TaskScheduler.UnobservedTaskException += TaskCrashed;
|
||||||
// Ensure proper encodings are available for MimeKit
|
// Ensure proper encodings are available for MimeKit
|
||||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
@@ -237,6 +241,12 @@ namespace Wino.Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TaskCrashed(object sender, UnobservedTaskExceptionEventArgs e) => Log.Error(e.Exception, "Task crashed.");
|
||||||
|
|
||||||
|
private void UIThreadCrash(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) => Log.Error(e.Exception, "UI thread crashed.");
|
||||||
|
|
||||||
|
private void ServerCrashed(object sender, UnhandledExceptionEventArgs e) => Log.Error((Exception)e.ExceptionObject, "Server crashed.");
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
notifyIcon?.Dispose();
|
notifyIcon?.Dispose();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace Wino.Server.Core
|
|||||||
nameof(ServerTerminationModeChanged) => App.Current.Services.GetService<ServerTerminationModeHandler>(),
|
nameof(ServerTerminationModeChanged) => App.Current.Services.GetService<ServerTerminationModeHandler>(),
|
||||||
nameof(TerminateServerRequested) => App.Current.Services.GetService<TerminateServerRequestHandler>(),
|
nameof(TerminateServerRequested) => App.Current.Services.GetService<TerminateServerRequestHandler>(),
|
||||||
nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService<ImapConnectivityTestHandler>(),
|
nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService<ImapConnectivityTestHandler>(),
|
||||||
|
nameof(KillAccountSynchronizerRequested) => App.Current.Services.GetService<KillAccountSynchronizerHandler>(),
|
||||||
_ => throw new Exception($"Server handler for {typeName} is not registered."),
|
_ => throw new Exception($"Server handler for {typeName} is not registered."),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ namespace Wino.Server.Core
|
|||||||
serviceCollection.AddTransient<ServerTerminationModeHandler>();
|
serviceCollection.AddTransient<ServerTerminationModeHandler>();
|
||||||
serviceCollection.AddTransient<TerminateServerRequestHandler>();
|
serviceCollection.AddTransient<TerminateServerRequestHandler>();
|
||||||
serviceCollection.AddTransient<ImapConnectivityTestHandler>();
|
serviceCollection.AddTransient<ImapConnectivityTestHandler>();
|
||||||
|
serviceCollection.AddTransient<KillAccountSynchronizerHandler>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Server;
|
||||||
|
using Wino.Messaging.Server;
|
||||||
|
using Wino.Server.Core;
|
||||||
|
|
||||||
|
namespace Wino.Server.MessageHandlers
|
||||||
|
{
|
||||||
|
public class KillAccountSynchronizerHandler : ServerMessageHandler<KillAccountSynchronizerRequested, bool>
|
||||||
|
{
|
||||||
|
private readonly ISynchronizerFactory _synchronizerFactory;
|
||||||
|
|
||||||
|
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex)
|
||||||
|
=> WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
|
||||||
|
|
||||||
|
public KillAccountSynchronizerHandler(ISynchronizerFactory synchronizerFactory)
|
||||||
|
{
|
||||||
|
_synchronizerFactory = synchronizerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<WinoServerResponse<bool>> HandleAsync(KillAccountSynchronizerRequested message, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _synchronizerFactory.DeleteSynchronizerAsync(message.AccountId);
|
||||||
|
|
||||||
|
return WinoServerResponse<bool>.CreateSuccessResponse(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ namespace Wino.Server.MessageHandlers
|
|||||||
|
|
||||||
bool shouldReportSynchronizationResult =
|
bool shouldReportSynchronizationResult =
|
||||||
message.Options.Type != MailSynchronizationType.ExecuteRequests &&
|
message.Options.Type != MailSynchronizationType.ExecuteRequests &&
|
||||||
|
message.Options.Type != MailSynchronizationType.IMAPIdle &&
|
||||||
message.Source == SynchronizationSource.Client;
|
message.Source == SynchronizationSource.Client;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -64,6 +65,12 @@ namespace Wino.Server.MessageHandlers
|
|||||||
|
|
||||||
var isSynchronizationSucceeded = synchronizationResult.CompletedState == SynchronizationCompletedState.Success;
|
var isSynchronizationSucceeded = synchronizationResult.CompletedState == SynchronizationCompletedState.Success;
|
||||||
|
|
||||||
|
// IDLE requests might be canceled successfully.
|
||||||
|
if (message.Options.Type == MailSynchronizationType.IMAPIdle && synchronizationResult.CompletedState == SynchronizationCompletedState.Canceled)
|
||||||
|
{
|
||||||
|
isSynchronizationSucceeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Update badge count of the notification task.
|
// Update badge count of the notification task.
|
||||||
if (isSynchronizationSucceeded)
|
if (isSynchronizationSucceeded)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ namespace Wino.Server
|
|||||||
IRecipient<ServerTerminationModeChanged>,
|
IRecipient<ServerTerminationModeChanged>,
|
||||||
IRecipient<AccountSynchronizationProgressUpdatedMessage>,
|
IRecipient<AccountSynchronizationProgressUpdatedMessage>,
|
||||||
IRecipient<AccountFolderConfigurationUpdated>,
|
IRecipient<AccountFolderConfigurationUpdated>,
|
||||||
IRecipient<CopyAuthURLRequested>
|
IRecipient<CopyAuthURLRequested>,
|
||||||
|
IRecipient<NewMailSynchronizationRequested>
|
||||||
{
|
{
|
||||||
private readonly System.Timers.Timer _timer;
|
private readonly System.Timers.Timer _timer;
|
||||||
private static object connectionLock = new object();
|
private static object connectionLock = new object();
|
||||||
@@ -140,6 +141,7 @@ namespace Wino.Server
|
|||||||
public async void Receive(AccountFolderConfigurationUpdated message) => await SendMessageAsync(MessageType.UIMessage, message);
|
public async void Receive(AccountFolderConfigurationUpdated message) => await SendMessageAsync(MessageType.UIMessage, message);
|
||||||
|
|
||||||
public async void Receive(CopyAuthURLRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
|
public async void Receive(CopyAuthURLRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
|
||||||
|
public async void Receive(NewMailSynchronizationRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -321,6 +323,9 @@ namespace Wino.Server
|
|||||||
|
|
||||||
KillServer();
|
KillServer();
|
||||||
break;
|
break;
|
||||||
|
case nameof(KillAccountSynchronizerRequested):
|
||||||
|
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<KillAccountSynchronizerRequested>(messageJson, _jsonSerializerOptions));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
|
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ namespace Wino.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ReportUIChange(new AccountRemovedMessage(account));
|
ReportUIChange(new AccountRemovedMessage(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +504,7 @@ namespace Wino.Services
|
|||||||
account.Preferences = preferences;
|
account.Preferences = preferences;
|
||||||
|
|
||||||
// Outlook & Office 365 supports Focused inbox. Enabled by default.
|
// Outlook & Office 365 supports Focused inbox. Enabled by default.
|
||||||
bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365;
|
bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook;
|
||||||
|
|
||||||
// TODO: This should come from account settings API.
|
// TODO: This should come from account settings API.
|
||||||
// Wino doesn't have MailboxSettings yet.
|
// Wino doesn't have MailboxSettings yet.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using MimeKit;
|
|||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Services.Extensions;
|
|
||||||
|
|
||||||
namespace Wino.Services.Extensions
|
namespace Wino.Services.Extensions
|
||||||
{
|
{
|
||||||
@@ -22,6 +21,9 @@ namespace Wino.Services.Extensions
|
|||||||
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
|
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UniqueId ResolveUidStruct(string mailCopyId)
|
||||||
|
=> new UniqueId(ResolveUid(mailCopyId));
|
||||||
|
|
||||||
public static string CreateUid(Guid folderId, uint messageUid)
|
public static string CreateUid(Guid folderId, uint messageUid)
|
||||||
=> $"{folderId}{MailCopyUidSeparator}{messageUid}";
|
=> $"{folderId}{MailCopyUidSeparator}{messageUid}";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user