Initial commit.
This commit is contained in:
403
Wino.Core/Services/AccountService.cs
Normal file
403
Wino.Core/Services/AccountService.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Messages.Accounts;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class AccountService : BaseDatabaseService, IAccountService
|
||||
{
|
||||
public IAuthenticator ExternalAuthenticationAuthenticator { get; set; }
|
||||
|
||||
private readonly IAuthenticationProvider _authenticationProvider;
|
||||
private readonly ISignatureService _signatureService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<AccountService>();
|
||||
|
||||
public AccountService(IDatabaseService databaseService,
|
||||
IAuthenticationProvider authenticationProvider,
|
||||
ISignatureService signatureService,
|
||||
IPreferencesService preferencesService) : base(databaseService)
|
||||
{
|
||||
_authenticationProvider = authenticationProvider;
|
||||
_signatureService = signatureService;
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
|
||||
|
||||
public async Task ClearAccountAttentionAsync(Guid accountId)
|
||||
{
|
||||
var account = await GetAccountAsync(accountId);
|
||||
|
||||
Guard.IsNotNull(account);
|
||||
|
||||
account.AttentionReason = AccountAttentionReason.None;
|
||||
|
||||
await UpdateAccountAsync(account);
|
||||
}
|
||||
|
||||
public async Task UpdateMergedInboxAsync(Guid mergedInboxId, IEnumerable<Guid> linkedAccountIds)
|
||||
{
|
||||
// First, remove all accounts from merged inbox.
|
||||
await Connection.ExecuteAsync("UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", mergedInboxId);
|
||||
|
||||
// Then, add new accounts to merged inbox.
|
||||
var query = new Query("MailAccount")
|
||||
.WhereIn("Id", linkedAccountIds)
|
||||
.AsUpdate(new
|
||||
{
|
||||
MergedInboxId = mergedInboxId
|
||||
});
|
||||
|
||||
await Connection.ExecuteAsync(query.GetRawQuery());
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
|
||||
}
|
||||
|
||||
public async Task UnlinkMergedInboxAsync(Guid mergedInboxId)
|
||||
{
|
||||
var mergedInbox = await Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId).ConfigureAwait(false);
|
||||
|
||||
if (mergedInbox == null)
|
||||
{
|
||||
_logger.Warning("Could not find merged inbox with id {MergedInboxId}", mergedInboxId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var query = new Query("MailAccount")
|
||||
.Where("MergedInboxId", mergedInboxId)
|
||||
.AsUpdate(new
|
||||
{
|
||||
MergedInboxId = (Guid?)null
|
||||
});
|
||||
|
||||
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
|
||||
await Connection.DeleteAsync(mergedInbox).ConfigureAwait(false);
|
||||
|
||||
// Change the startup entity id if it was the merged inbox.
|
||||
// Take the first account as startup account.
|
||||
|
||||
if (_preferencesService.StartupEntityId == mergedInboxId)
|
||||
{
|
||||
var firstAccount = await Connection.Table<MailAccount>().FirstOrDefaultAsync();
|
||||
|
||||
if (firstAccount != null)
|
||||
{
|
||||
_preferencesService.StartupEntityId = firstAccount.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
_preferencesService.StartupEntityId = null;
|
||||
}
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
|
||||
}
|
||||
|
||||
public async Task CreateMergeAccountsAsync(MergedInbox mergedInbox, IEnumerable<MailAccount> accountsToMerge)
|
||||
{
|
||||
if (mergedInbox == null) return;
|
||||
|
||||
// 0. Give the merged inbox a new Guid.
|
||||
mergedInbox.Id = Guid.NewGuid();
|
||||
|
||||
var accountFolderDictionary = new Dictionary<MailAccount, List<MailItemFolder>>();
|
||||
|
||||
// 1. Make all folders in the accounts unsticky. We will stick them based on common special folder types.
|
||||
foreach (var account in accountsToMerge)
|
||||
{
|
||||
var accountFolderList = new List<MailItemFolder>();
|
||||
|
||||
var folders = await Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == account.Id).ToListAsync();
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
accountFolderList.Add(folder);
|
||||
folder.IsSticky = false;
|
||||
|
||||
await Connection.UpdateAsync(folder);
|
||||
}
|
||||
|
||||
accountFolderDictionary.Add(account, accountFolderList);
|
||||
}
|
||||
|
||||
// 2. Find the common special folders and stick them.
|
||||
// Only following types will be considered as common special folder.
|
||||
SpecialFolderType[] commonSpecialTypes =
|
||||
[
|
||||
SpecialFolderType.Inbox,
|
||||
SpecialFolderType.Sent,
|
||||
SpecialFolderType.Draft,
|
||||
SpecialFolderType.Archive,
|
||||
SpecialFolderType.Junk,
|
||||
SpecialFolderType.Deleted
|
||||
];
|
||||
|
||||
foreach (var type in commonSpecialTypes)
|
||||
{
|
||||
var isCommonType = accountFolderDictionary
|
||||
.Select(a => a.Value)
|
||||
.Where(a => a.Any(a => a.SpecialFolderType == type))
|
||||
.Count() == accountsToMerge.Count();
|
||||
|
||||
if (isCommonType)
|
||||
{
|
||||
foreach (var account in accountsToMerge)
|
||||
{
|
||||
var folder = accountFolderDictionary[account].FirstOrDefault(a => a.SpecialFolderType == type);
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
folder.IsSticky = true;
|
||||
|
||||
await Connection.UpdateAsync(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Insert merged inbox and assign accounts.
|
||||
await Connection.InsertAsync(mergedInbox);
|
||||
|
||||
foreach (var account in accountsToMerge)
|
||||
{
|
||||
account.MergedInboxId = mergedInbox.Id;
|
||||
|
||||
await Connection.UpdateAsync(account);
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
|
||||
}
|
||||
|
||||
public async Task RenameMergedAccountAsync(Guid mergedInboxId, string newName)
|
||||
{
|
||||
var query = new Query("MergedInbox")
|
||||
.Where("Id", mergedInboxId)
|
||||
.AsUpdate(new
|
||||
{
|
||||
Name = newName
|
||||
});
|
||||
|
||||
await Connection.ExecuteAsync(query.GetRawQuery());
|
||||
|
||||
ReportUIChange(new MergedInboxRenamed(mergedInboxId, newName));
|
||||
}
|
||||
|
||||
public async Task FixTokenIssuesAsync(Guid accountId)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (account == null) return;
|
||||
|
||||
var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);
|
||||
|
||||
// This will re-generate token.
|
||||
var token = await authenticator.GenerateTokenAsync(account, true);
|
||||
|
||||
Guard.IsNotNull(token);
|
||||
}
|
||||
|
||||
private Task<MailAccountPreferences> GetAccountPreferencesAsync(Guid accountId)
|
||||
=> Connection.Table<MailAccountPreferences>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
||||
|
||||
public async Task<List<MailAccount>> GetAccountsAsync()
|
||||
{
|
||||
var accounts = await Connection.Table<MailAccount>().ToListAsync();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
// Load IMAP server configuration.
|
||||
if (account.ProviderType == MailProviderType.IMAP4)
|
||||
account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id);
|
||||
|
||||
// Load MergedInbox information.
|
||||
if (account.MergedInboxId != null)
|
||||
account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value);
|
||||
|
||||
account.Preferences = await GetAccountPreferencesAsync(account.Id);
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
|
||||
=> Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
|
||||
|
||||
public async Task DeleteAccountAsync(MailAccount account)
|
||||
{
|
||||
// TODO: Delete mime messages and attachments.
|
||||
|
||||
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);
|
||||
|
||||
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
|
||||
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
|
||||
|
||||
if (account.SignatureId != null)
|
||||
await Connection.Table<AccountSignature>().DeleteAsync(a => a.Id == account.SignatureId);
|
||||
|
||||
// Account belongs to a merged inbox.
|
||||
// In case of there'll be a single account in the merged inbox, remove the merged inbox as well.
|
||||
|
||||
if (account.MergedInboxId != null)
|
||||
{
|
||||
var mergedInboxAccountCount = await Connection.Table<MailAccount>().Where(a => a.MergedInboxId == account.MergedInboxId.Value).CountAsync();
|
||||
|
||||
// There will be only one account in the merged inbox. Remove the link for the other account as well.
|
||||
if (mergedInboxAccountCount == 2)
|
||||
{
|
||||
var query = new Query("MailAccount")
|
||||
.Where("MergedInboxId", account.MergedInboxId.Value)
|
||||
.AsUpdate(new
|
||||
{
|
||||
MergedInboxId = (Guid?)null
|
||||
});
|
||||
|
||||
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (account.ProviderType == MailProviderType.IMAP4)
|
||||
await Connection.Table<CustomServerInformation>().DeleteAsync(a => a.AccountId == account.Id);
|
||||
|
||||
if (account.Preferences != null)
|
||||
await Connection.DeleteAsync(account.Preferences);
|
||||
|
||||
await Connection.DeleteAsync(account);
|
||||
|
||||
// Clear out or set up a new startup entity id.
|
||||
// Next account after the deleted one will be the startup account.
|
||||
|
||||
if (_preferencesService.StartupEntityId == account.Id || _preferencesService.StartupEntityId == account.MergedInboxId)
|
||||
{
|
||||
var firstNonStartupAccount = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id != account.Id);
|
||||
|
||||
if (firstNonStartupAccount != null)
|
||||
{
|
||||
_preferencesService.StartupEntityId = firstNonStartupAccount.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
_preferencesService.StartupEntityId = null;
|
||||
}
|
||||
}
|
||||
|
||||
ReportUIChange(new AccountRemovedMessage(account));
|
||||
}
|
||||
|
||||
public async Task<MailAccount> GetAccountAsync(Guid accountId)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (account?.ProviderType == MailProviderType.IMAP4)
|
||||
account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id);
|
||||
|
||||
account.Preferences = await GetAccountPreferencesAsync(account.Id);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public Task<CustomServerInformation> GetAccountCustomServerInformationAsync(Guid accountId)
|
||||
=> Connection.Table<CustomServerInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
||||
|
||||
public async Task UpdateAccountAsync(MailAccount account)
|
||||
{
|
||||
if (account.Preferences == null)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
await Connection.UpdateAsync(account.Preferences);
|
||||
await Connection.UpdateAsync(account);
|
||||
|
||||
ReportUIChange(new AccountUpdatedMessage(account));
|
||||
}
|
||||
|
||||
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
|
||||
{
|
||||
Guard.IsNotNull(account);
|
||||
|
||||
var accountCount = await Connection.Table<MailAccount>().CountAsync();
|
||||
|
||||
// If there are no accounts before this one, set it as startup account.
|
||||
if (accountCount == 0)
|
||||
{
|
||||
_preferencesService.StartupEntityId = account.Id;
|
||||
}
|
||||
|
||||
await Connection.InsertAsync(account);
|
||||
|
||||
var preferences = new MailAccountPreferences()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = account.Id,
|
||||
IsNotificationsEnabled = true,
|
||||
ShouldAppendMessagesToSentFolder = false
|
||||
};
|
||||
|
||||
account.Preferences = preferences;
|
||||
|
||||
// Outlook & Office 365 supports Focused inbox. Enabled by default.
|
||||
bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365;
|
||||
|
||||
if (isMicrosoftProvider)
|
||||
account.Preferences.IsFocusedInboxEnabled = true;
|
||||
|
||||
await Connection.InsertAsync(preferences);
|
||||
|
||||
// Create default signature.
|
||||
var defaultSignature = await _signatureService.CreateDefaultSignatureAsync(account.Id);
|
||||
|
||||
account.SignatureId = defaultSignature.Id;
|
||||
|
||||
if (customServerInformation != null)
|
||||
await Connection.InsertAsync(customServerInformation);
|
||||
|
||||
if (tokenInformation != null)
|
||||
await Connection.InsertAsync(tokenInformation);
|
||||
}
|
||||
|
||||
public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
|
||||
{
|
||||
var account = await GetAccountAsync(accountId);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
_logger.Error("Could not find account with id {AccountId}", accountId);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var currentIdentifier = account.SynchronizationDeltaIdentifier;
|
||||
|
||||
bool shouldUpdateIdentifier = account.ProviderType == MailProviderType.Gmail ?
|
||||
((string.IsNullOrEmpty(currentIdentifier) ? true : !string.IsNullOrEmpty(currentIdentifier)
|
||||
&& ulong.TryParse(currentIdentifier, out ulong currentIdentifierValue)
|
||||
&& ulong.TryParse(newIdentifier, out ulong newIdentifierValue)
|
||||
&& newIdentifierValue > currentIdentifierValue)) : true;
|
||||
|
||||
if (shouldUpdateIdentifier)
|
||||
{
|
||||
_logger.Debug("Updating synchronization identifier for {Name}. From: {SynchronizationDeltaIdentifier} To: {NewIdentifier}", account.Name, account.SynchronizationDeltaIdentifier, newIdentifier);
|
||||
account.SynchronizationDeltaIdentifier = newIdentifier;
|
||||
|
||||
await UpdateAccountAsync(account);
|
||||
}
|
||||
|
||||
return account.SynchronizationDeltaIdentifier;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
34
Wino.Core/Services/AuthenticationProvider.cs
Normal file
34
Wino.Core/Services/AuthenticationProvider.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Wino.Core.Authenticators;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using IAuthenticationProvider = Wino.Core.Domain.Interfaces.IAuthenticationProvider;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class AuthenticationProvider : IAuthenticationProvider
|
||||
{
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly ITokenService _tokenService;
|
||||
|
||||
public AuthenticationProvider(INativeAppService nativeAppService, ITokenService tokenService)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
public IAuthenticator GetAuthenticator(MailProviderType providerType)
|
||||
{
|
||||
return providerType switch
|
||||
{
|
||||
MailProviderType.Outlook => new OutlookAuthenticator(_tokenService, _nativeAppService),
|
||||
MailProviderType.Office365 => new Office365Authenticator(_tokenService, _nativeAppService),
|
||||
MailProviderType.Gmail => new GmailAuthenticator(_tokenService, _nativeAppService),
|
||||
MailProviderType.Yahoo => new YahooAuthenticator(_tokenService),
|
||||
MailProviderType.IMAP4 => new CustomAuthenticator(_tokenService),
|
||||
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Wino.Core/Services/AutoDiscoveryService.cs
Normal file
56
Wino.Core/Services/AutoDiscoveryService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.AutoDiscovery;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// We have 2 methods to do auto discovery.
|
||||
/// 1. Use https://emailsettings.firetrust.com/settings?q={address} API
|
||||
/// 2. TODO: Thunderbird auto discovery file.
|
||||
/// </summary>
|
||||
public class AutoDiscoveryService : IAutoDiscoveryService
|
||||
{
|
||||
private const string FiretrustURL = " https://emailsettings.firetrust.com/settings?q=";
|
||||
|
||||
// TODO: Try Thunderbird Auto Discovery as second approach.
|
||||
|
||||
public Task<AutoDiscoverySettings> GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings)
|
||||
=> GetSettingsFromFiretrustAsync(autoDiscoveryMinimalSettings.Email);
|
||||
|
||||
private async Task<AutoDiscoverySettings> GetSettingsFromFiretrustAsync(string mailAddress)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync($"{FiretrustURL}{mailAddress}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await DeserializeFiretrustResponse(response);
|
||||
else
|
||||
{
|
||||
Log.Warning($"Firetrust AutoDiscovery failed. ({response.StatusCode})");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AutoDiscoverySettings> DeserializeFiretrustResponse(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<AutoDiscoverySettings>(content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to deserialize Firetrust response.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Wino.Core/Services/BaseDatabaseService.cs
Normal file
22
Wino.Core/Services/BaseDatabaseService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class BaseDatabaseService
|
||||
{
|
||||
protected IMessenger Messenger => WeakReferenceMessenger.Default;
|
||||
protected SQLiteAsyncConnection Connection => _databaseService.Connection;
|
||||
|
||||
private readonly IDatabaseService _databaseService;
|
||||
|
||||
public BaseDatabaseService(IDatabaseService databaseService)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage
|
||||
=> Messenger.Send(message);
|
||||
}
|
||||
}
|
||||
54
Wino.Core/Services/ContactService.cs
Normal file
54
Wino.Core/Services/ContactService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public interface IContactService
|
||||
{
|
||||
Task<List<AddressInformation>> GetAddressInformationAsync(string queryText);
|
||||
Task<AddressInformation> GetAddressInformationByAddressAsync(string address);
|
||||
Task SaveAddressInformationAsync(MimeMessage message);
|
||||
}
|
||||
|
||||
public class ContactService : BaseDatabaseService, IContactService
|
||||
{
|
||||
public ContactService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public Task<List<AddressInformation>> GetAddressInformationAsync(string queryText)
|
||||
{
|
||||
if (queryText == null || queryText.Length < 2)
|
||||
return Task.FromResult<List<AddressInformation>>(null);
|
||||
|
||||
var query = new Query(nameof(AddressInformation));
|
||||
query.WhereContains("Address", queryText);
|
||||
query.OrWhereContains("Name", queryText);
|
||||
|
||||
var rawLikeQuery = query.GetRawQuery();
|
||||
|
||||
return Connection.QueryAsync<AddressInformation>(rawLikeQuery);
|
||||
}
|
||||
|
||||
public async Task<AddressInformation> GetAddressInformationByAddressAsync(string address)
|
||||
{
|
||||
return await Connection.Table<AddressInformation>().Where(a => a.Address == address).FirstOrDefaultAsync()
|
||||
?? new AddressInformation() { Name = address, Address = address };
|
||||
}
|
||||
|
||||
public async Task SaveAddressInformationAsync(MimeMessage message)
|
||||
{
|
||||
var recipients = message
|
||||
.GetRecipients(true)
|
||||
.Where(a => !string.IsNullOrEmpty(a.Name) && !string.IsNullOrEmpty(a.Address));
|
||||
|
||||
var addressInformations = recipients.Select(a => new AddressInformation() { Name = a.Name, Address = a.Address });
|
||||
|
||||
foreach (var info in addressInformations)
|
||||
await Connection.InsertOrReplaceAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
182
Wino.Core/Services/ContextMenuItemService.cs
Normal file
182
Wino.Core/Services/ContextMenuItemService.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class ContextMenuItemService : IContextMenuItemService
|
||||
{
|
||||
public virtual IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation)
|
||||
{
|
||||
var list = new List<FolderOperationMenuItem>();
|
||||
|
||||
if (folderInformation.IsSticky)
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Unpin));
|
||||
else
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Pin));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
// Following 4 items are disabled for system folders.
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Rename, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Delete, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.CreateSubFolder, !folderInformation.IsSystemFolder));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Empty));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.MarkAllAsRead));
|
||||
|
||||
return list;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems)
|
||||
{
|
||||
if (selectedMailItems == null)
|
||||
return default;
|
||||
|
||||
var operationList = new List<MailOperationMenuItem>();
|
||||
|
||||
// Disable archive button for Archive folder itself.
|
||||
|
||||
bool isArchiveFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive);
|
||||
bool isDraftOrSent = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || a.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent);
|
||||
bool isJunkFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk);
|
||||
|
||||
bool isSingleItem = selectedMailItems.Count() == 1;
|
||||
|
||||
IMailItem singleItem = selectedMailItems.FirstOrDefault();
|
||||
|
||||
// Archive button.
|
||||
|
||||
if (isArchiveFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Move button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Move, !isDraftOrSent));
|
||||
|
||||
// Independent flag, read etc.
|
||||
if (isSingleItem)
|
||||
{
|
||||
if (singleItem.IsFlagged)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
if (singleItem.IsRead)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead));
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isAllFlagged = selectedMailItems.All(a => a.IsFlagged);
|
||||
bool isAllRead = selectedMailItems.All(a => a.IsRead);
|
||||
bool isAllUnread = selectedMailItems.All(a => !a.IsRead);
|
||||
|
||||
if (isAllRead)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread));
|
||||
else
|
||||
{
|
||||
if (!isAllUnread)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread));
|
||||
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead));
|
||||
}
|
||||
|
||||
if (isAllFlagged)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
{
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore
|
||||
if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Ignore));
|
||||
|
||||
// Seperator
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// Junk folder
|
||||
if (isJunkFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsNotJunk));
|
||||
else if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MoveToJunk));
|
||||
|
||||
// TODO: Focus folder support.
|
||||
|
||||
// Remove the separator if it's the last item remaining.
|
||||
// It's creating unpleasent UI glitch.
|
||||
|
||||
if (operationList.LastOrDefault()?.Operation == MailOperation.Seperator)
|
||||
operationList.RemoveAt(operationList.Count - 1);
|
||||
|
||||
return operationList;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor)
|
||||
{
|
||||
var actionList = new List<MailOperationMenuItem>();
|
||||
|
||||
bool isArchiveFolder = mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive;
|
||||
|
||||
// Add light/dark editor theme switch.
|
||||
if (isDarkEditor)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
||||
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// You can't do these to draft items.
|
||||
if (!mailItem.IsDraft)
|
||||
{
|
||||
// Reply
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Reply));
|
||||
|
||||
// Reply All
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ReplyAll));
|
||||
|
||||
// Forward
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Forward));
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (isArchiveFolder)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Flag - Clear Flag
|
||||
if (mailItem.IsFlagged)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
// Secondary items.
|
||||
|
||||
// Read - Unread
|
||||
if (mailItem.IsRead)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread, true, false));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
||||
|
||||
return actionList;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Wino.Core/Services/DatabaseService.cs
Normal file
69
Wino.Core/Services/DatabaseService.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public interface IDatabaseService : IInitializeAsync
|
||||
{
|
||||
SQLiteAsyncConnection Connection { get; }
|
||||
}
|
||||
|
||||
public class DatabaseService : IDatabaseService
|
||||
{
|
||||
private string DatabaseName => "Wino.db";
|
||||
|
||||
private bool _isInitialized = false;
|
||||
private readonly IAppInitializerService _appInitializerService;
|
||||
|
||||
public SQLiteAsyncConnection Connection { get; private set; }
|
||||
|
||||
public DatabaseService(IAppInitializerService appInitializerService)
|
||||
{
|
||||
_appInitializerService = appInitializerService;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_isInitialized)
|
||||
return;
|
||||
|
||||
var applicationData = _appInitializerService.GetApplicationDataFolder();
|
||||
var databaseFileName = Path.Combine(applicationData, DatabaseName);
|
||||
|
||||
Connection = new SQLiteAsyncConnection(databaseFileName)
|
||||
{
|
||||
// Enable for debugging sqlite.
|
||||
Trace = true,
|
||||
Tracer = new Action<string>((t) =>
|
||||
{
|
||||
// Debug.WriteLine(t);
|
||||
// Log.Debug(t);
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
await CreateTablesAsync();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private async Task CreateTablesAsync()
|
||||
{
|
||||
await Connection.CreateTablesAsync(CreateFlags.None,
|
||||
typeof(MailCopy),
|
||||
typeof(MailItemFolder),
|
||||
typeof(MailAccount),
|
||||
typeof(TokenInformation),
|
||||
typeof(AddressInformation),
|
||||
typeof(CustomServerInformation),
|
||||
typeof(AccountSignature),
|
||||
typeof(MergedInbox),
|
||||
typeof(MailAccountPreferences)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
592
Wino.Core/Services/FolderService.cs
Normal file
592
Wino.Core/Services/FolderService.cs
Normal file
@@ -0,0 +1,592 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class FolderService : BaseDatabaseService, IFolderService
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly ILogger _logger = Log.ForContext<FolderService>();
|
||||
|
||||
private readonly SpecialFolderType[] gmailCategoryFolderTypes =
|
||||
[
|
||||
SpecialFolderType.Promotions,
|
||||
SpecialFolderType.Social,
|
||||
SpecialFolderType.Updates,
|
||||
SpecialFolderType.Forums,
|
||||
SpecialFolderType.Personal
|
||||
];
|
||||
|
||||
public FolderService(IDatabaseService databaseService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : base(databaseService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_mimeFileService = mimeFileService;
|
||||
}
|
||||
|
||||
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
|
||||
=> await Connection.ExecuteAsync("UPDATE MailItemFolder SET IsSticky = ? WHERE Id = ?", isSticky, folderId);
|
||||
|
||||
public async Task<int> GetFolderNotificationBadgeAsync(Guid folderId)
|
||||
{
|
||||
var folder = await GetFolderAsync(folderId);
|
||||
|
||||
if (folder == null || !folder.ShowUnreadCount) return default;
|
||||
|
||||
var account = await _accountService.GetAccountAsync(folder.MailAccountId);
|
||||
|
||||
if (account == null) return default;
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Where("FolderId", folderId)
|
||||
.SelectRaw("count (DISTINCT Id)");
|
||||
|
||||
// If focused inbox is enabled, we need to check if this is the inbox folder.
|
||||
if (account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault() && folder.SpecialFolderType == SpecialFolderType.Inbox)
|
||||
{
|
||||
query.Where("IsFocused", 1);
|
||||
}
|
||||
|
||||
// Draft and Junk folders are not counted as unread. They must return the item count instead.
|
||||
|
||||
if (folder.SpecialFolderType != SpecialFolderType.Draft || folder.SpecialFolderType != SpecialFolderType.Junk)
|
||||
{
|
||||
query.Where("IsRead", 0);
|
||||
}
|
||||
|
||||
return await Connection.ExecuteScalarAsync<int>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<AccountFolderTree> GetFolderStructureForAccountAsync(Guid accountId, bool includeHiddenFolders)
|
||||
{
|
||||
var account = await _accountService.GetAccountAsync(accountId);
|
||||
|
||||
if (account == null)
|
||||
throw new ArgumentException(nameof(account));
|
||||
|
||||
var accountTree = new AccountFolderTree(account);
|
||||
|
||||
// Account folders.
|
||||
var folderQuery = Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId);
|
||||
|
||||
if (!includeHiddenFolders)
|
||||
folderQuery = folderQuery.Where(a => !a.IsHidden);
|
||||
|
||||
// Load child folders for each folder.
|
||||
var allFolders = await folderQuery.OrderBy(a => a.SpecialFolderType).ToListAsync();
|
||||
|
||||
if (allFolders.Any())
|
||||
{
|
||||
// Get sticky folders. Category type is always sticky.
|
||||
// Sticky folders don't have tree structure. So they can be added to the main tree.
|
||||
var stickyFolders = allFolders.Where(a => a.IsSticky && a.SpecialFolderType != SpecialFolderType.Category);
|
||||
|
||||
foreach (var stickyFolder in stickyFolders)
|
||||
{
|
||||
var childStructure = await GetChildFolderItemsRecursiveAsync(stickyFolder.Id, accountId);
|
||||
|
||||
accountTree.Folders.Add(childStructure);
|
||||
}
|
||||
|
||||
// Check whether we need special 'Categories' kind of folder.
|
||||
var categoryExists = allFolders.Any(a => a.SpecialFolderType == SpecialFolderType.Category);
|
||||
|
||||
if (categoryExists)
|
||||
{
|
||||
var categoryFolder = allFolders.First(a => a.SpecialFolderType == SpecialFolderType.Category);
|
||||
|
||||
// Construct category items under pinned items.
|
||||
var categoryFolders = allFolders.Where(a => gmailCategoryFolderTypes.Contains(a.SpecialFolderType));
|
||||
|
||||
foreach (var categoryFolderSubItem in categoryFolders)
|
||||
{
|
||||
categoryFolder.ChildFolders.Add(categoryFolderSubItem);
|
||||
}
|
||||
|
||||
accountTree.Folders.Add(categoryFolder);
|
||||
allFolders.Remove(categoryFolder);
|
||||
}
|
||||
|
||||
// Move rest of the items into virtual More folder if any.
|
||||
var nonStickyFolders = allFolders.Except(stickyFolders);
|
||||
|
||||
if (nonStickyFolders.Any())
|
||||
{
|
||||
var virtualMoreFolder = new MailItemFolder()
|
||||
{
|
||||
FolderName = Translator.More,
|
||||
SpecialFolderType = SpecialFolderType.More
|
||||
};
|
||||
|
||||
foreach (var unstickyItem in nonStickyFolders)
|
||||
{
|
||||
if (account.ProviderType == MailProviderType.Gmail)
|
||||
{
|
||||
// Gmail requires this check to not include child folders as
|
||||
// separate folder without their parent for More folder...
|
||||
|
||||
if (!string.IsNullOrEmpty(unstickyItem.ParentRemoteFolderId))
|
||||
continue;
|
||||
}
|
||||
else if (account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365)
|
||||
{
|
||||
bool belongsToExistingParent = (await Connection
|
||||
.Table<MailItemFolder>()
|
||||
.Where(a => unstickyItem.ParentRemoteFolderId == a.RemoteFolderId)
|
||||
.CountAsync()) > 0;
|
||||
|
||||
// No need to include this as unsticky.
|
||||
if (belongsToExistingParent) continue;
|
||||
}
|
||||
|
||||
var structure = await GetChildFolderItemsRecursiveAsync(unstickyItem.Id, accountId);
|
||||
|
||||
virtualMoreFolder.ChildFolders.Add(structure);
|
||||
}
|
||||
|
||||
// Only add more if there are any.
|
||||
if (virtualMoreFolder.ChildFolders.Count > 0)
|
||||
accountTree.Folders.Add(virtualMoreFolder);
|
||||
}
|
||||
}
|
||||
|
||||
return accountTree;
|
||||
}
|
||||
|
||||
private async Task<MailItemFolder> GetChildFolderItemsRecursiveAsync(Guid folderId, Guid accountId)
|
||||
{
|
||||
var folder = await Connection.Table<MailItemFolder>().Where(a => a.Id == folderId && a.MailAccountId == accountId).FirstOrDefaultAsync();
|
||||
|
||||
if (folder == null)
|
||||
return null;
|
||||
|
||||
var childFolders = await Connection.Table<MailItemFolder>()
|
||||
.Where(a => a.ParentRemoteFolderId == folder.RemoteFolderId && a.MailAccountId == folder.MailAccountId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var childFolder in childFolders)
|
||||
{
|
||||
var subChild = await GetChildFolderItemsRecursiveAsync(childFolder.Id, accountId);
|
||||
folder.ChildFolders.Add(subChild);
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
public async Task<MailItemFolder> GetSpecialFolderByAccountIdAsync(Guid accountId, SpecialFolderType type)
|
||||
=> await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.SpecialFolderType == type);
|
||||
|
||||
public async Task<MailItemFolder> GetFolderAsync(Guid folderId)
|
||||
=> await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.Id.Equals(folderId));
|
||||
|
||||
public Task<int> GetCurrentItemCountForFolder(Guid folderId)
|
||||
=> Connection.Table<MailCopy>().Where(a => a.FolderId == folderId).CountAsync();
|
||||
|
||||
public Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId)
|
||||
=> Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId).ToListAsync();
|
||||
|
||||
public async Task UpdateCustomServerMailListAsync(Guid accountId, List<MailItemFolder> folders)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (account == null)
|
||||
return;
|
||||
|
||||
// IMAP servers don't have unique identifier for folders all the time.
|
||||
// We'll map them with parent-name relation.
|
||||
|
||||
var currentFolders = await GetFoldersAsync(accountId);
|
||||
|
||||
// These folders don't exist anymore. Remove them.
|
||||
var localRemoveFolders = currentFolders.ExceptBy(folders, a => a.RemoteFolderId);
|
||||
|
||||
foreach (var currentFolder in currentFolders)
|
||||
{
|
||||
// Check if we have this folder locally.
|
||||
var remotelyExistFolder = folders.FirstOrDefault(a => a.RemoteFolderId == currentFolder.RemoteFolderId
|
||||
&& a.ParentRemoteFolderId == currentFolder.ParentRemoteFolderId);
|
||||
|
||||
if (remotelyExistFolder == null)
|
||||
{
|
||||
// This folder is removed.
|
||||
// Remove everything for this folder.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var currentFolder = await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.RemoteFolderId == folder.RemoteFolderId);
|
||||
|
||||
// Nothing is changed, it's still the same folder.
|
||||
// Just update Id of the folder.
|
||||
|
||||
if (currentFolder != null)
|
||||
folder.Id = currentFolder.Id;
|
||||
|
||||
await Connection.InsertOrReplaceAsync(folder);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
|
||||
{
|
||||
var folder = await GetFolderAsync(folderId);
|
||||
|
||||
if (folder == null) return default;
|
||||
|
||||
var mailCopyIds = await GetMailCopyIdsByFolderIdAsync(folderId);
|
||||
|
||||
// Make sure we don't include Ids that doesn't have uid separator.
|
||||
// Local drafts might not have it for example.
|
||||
|
||||
return new List<uint>(mailCopyIds.Where(a => a.Contains(MailkitClientExtensions.MailCopyUidSeparator)).Select(a => MailkitClientExtensions.ResolveUid(a)));
|
||||
}
|
||||
|
||||
public async Task<MailAccount> UpdateSystemFolderConfigurationAsync(Guid accountId, SystemFolderConfiguration configuration)
|
||||
{
|
||||
if (configuration == null)
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
|
||||
var account = await _accountService.GetAccountAsync(accountId);
|
||||
|
||||
if (account == null)
|
||||
throw new ArgumentNullException(nameof(account));
|
||||
|
||||
// Update system folders for this account.
|
||||
|
||||
await Task.WhenAll(UpdateSystemFolderInternalAsync(configuration.SentFolder, SpecialFolderType.Sent),
|
||||
UpdateSystemFolderInternalAsync(configuration.DraftFolder, SpecialFolderType.Draft),
|
||||
UpdateSystemFolderInternalAsync(configuration.JunkFolder, SpecialFolderType.Junk),
|
||||
UpdateSystemFolderInternalAsync(configuration.TrashFolder, SpecialFolderType.Deleted));
|
||||
|
||||
await _accountService.UpdateAccountAsync(account);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
private Task UpdateSystemFolderInternalAsync(MailItemFolder folder, SpecialFolderType assignedSpecialFolderType)
|
||||
{
|
||||
if (folder == null) return Task.CompletedTask;
|
||||
|
||||
folder.IsSticky = true;
|
||||
folder.IsSynchronizationEnabled = true;
|
||||
folder.IsSystemFolder = true;
|
||||
folder.SpecialFolderType = assignedSpecialFolderType;
|
||||
|
||||
return UpdateFolderAsync(folder);
|
||||
}
|
||||
|
||||
public async Task ChangeFolderSynchronizationStateAsync(Guid folderId, bool isSynchronizationEnabled)
|
||||
{
|
||||
var localFolder = await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.Id == folderId);
|
||||
|
||||
if (localFolder != null)
|
||||
{
|
||||
localFolder.IsSynchronizationEnabled = isSynchronizationEnabled;
|
||||
|
||||
await UpdateFolderAsync(localFolder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
#region Repository Calls
|
||||
|
||||
public async Task InsertFolderAsync(MailItemFolder folder)
|
||||
{
|
||||
if (folder == null)
|
||||
{
|
||||
_logger.Warning("Folder is null. Cannot insert.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var account = await _accountService.GetAccountAsync(folder.MailAccountId);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
_logger.Warning("Account with id {MailAccountId} does not exist. Cannot insert folder.", folder.MailAccountId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var existingFolder = await GetFolderAsync(folder.Id).ConfigureAwait(false);
|
||||
|
||||
// IMAP servers don't have unique identifier for folders all the time.
|
||||
// So we'll try to match them with remote folder id and account id relation.
|
||||
// If we have a match, we'll update the folder instead of inserting.
|
||||
|
||||
existingFolder ??= await GetFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
if (existingFolder == null)
|
||||
{
|
||||
_logger.Debug("Inserting folder {Id} - {FolderName}", folder.Id, folder.FolderName, folder.MailAccountId);
|
||||
|
||||
await Connection.InsertAsync(folder).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new FolderAddedMessage(folder, account));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Folder {Id} - {FolderName} already exists. Updating.", folder.Id, folder.FolderName);
|
||||
|
||||
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateFolderAsync(MailItemFolder folder)
|
||||
{
|
||||
if (folder == null)
|
||||
{
|
||||
_logger.Warning("Folder is null. Cannot update.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var account = await _accountService.GetAccountAsync(folder.MailAccountId).ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
_logger.Warning("Account with id {MailAccountId} does not exist. Cannot update folder.", folder.MailAccountId);
|
||||
return;
|
||||
}
|
||||
|
||||
#if !DEBUG // Annoying
|
||||
_logger.Debug("Updating folder {FolderName}", folder.Id, folder.FolderName);
|
||||
#endif
|
||||
|
||||
await Connection.UpdateAsync(folder).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new FolderUpdatedMessage(folder, account));
|
||||
}
|
||||
|
||||
private async Task DeleteFolderAsync(MailItemFolder folder)
|
||||
{
|
||||
if (folder == null)
|
||||
{
|
||||
_logger.Warning("Folder is null. Cannot delete.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var account = await _accountService.GetAccountAsync(folder.MailAccountId).ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
_logger.Warning("Account with id {MailAccountId} does not exist. Cannot delete folder.", folder.MailAccountId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Deleting folder {FolderName}", folder.FolderName);
|
||||
|
||||
await Connection.DeleteAsync(folder).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new FolderRemovedMessage(folder, account));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Task<List<string>> GetMailCopyIdsByFolderIdAsync(Guid folderId)
|
||||
{
|
||||
var query = new Query("MailCopy")
|
||||
.Where("FolderId", folderId)
|
||||
.Select("Id");
|
||||
|
||||
return Connection.QueryScalarsAsync<string>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(IEnumerable<string> mailCopyIds)
|
||||
{
|
||||
// Get all assignments for all items.
|
||||
var query = new Query(nameof(MailCopy))
|
||||
.Join(nameof(MailItemFolder), $"{nameof(MailCopy)}.FolderId", $"{nameof(MailItemFolder)}.Id")
|
||||
.WhereIn($"{nameof(MailCopy)}.Id", mailCopyIds)
|
||||
.SelectRaw($"{nameof(MailCopy)}.Id as MailCopyId, {nameof(MailItemFolder)}.Id as FolderId, {nameof(MailItemFolder)}.RemoteFolderId as RemoteFolderId")
|
||||
.Distinct();
|
||||
|
||||
var rowQuery = query.GetRawQuery();
|
||||
|
||||
return await Connection.QueryAsync<MailFolderPairMetadata>(rowQuery);
|
||||
}
|
||||
|
||||
public Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(string mailCopyId)
|
||||
=> GetMailFolderPairMetadatasAsync(new List<string>() { mailCopyId });
|
||||
|
||||
public async Task SetSpecialFolderAsync(Guid folderId, SpecialFolderType type)
|
||||
=> await Connection.ExecuteAsync("UPDATE MailItemFolder SET SpecialFolderType = ? WHERE Id = ?", type, folderId);
|
||||
|
||||
public async Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options)
|
||||
{
|
||||
var folders = new List<MailItemFolder>();
|
||||
|
||||
if (options.Type == SynchronizationType.Inbox)
|
||||
{
|
||||
var inboxFolder = await GetSpecialFolderByAccountIdAsync(options.AccountId, SpecialFolderType.Inbox);
|
||||
var sentFolder = await GetSpecialFolderByAccountIdAsync(options.AccountId, SpecialFolderType.Sent);
|
||||
var draftFolder = await GetSpecialFolderByAccountIdAsync(options.AccountId, SpecialFolderType.Draft);
|
||||
|
||||
// For properly creating threads we need Sent and Draft to be synchronized as well.
|
||||
|
||||
if (sentFolder != null && sentFolder.IsSynchronizationEnabled)
|
||||
{
|
||||
folders.Add(sentFolder);
|
||||
}
|
||||
|
||||
if (draftFolder != null && draftFolder.IsSynchronizationEnabled)
|
||||
{
|
||||
folders.Add(draftFolder);
|
||||
}
|
||||
|
||||
// User might've disabled inbox synchronization somehow...
|
||||
if (inboxFolder != null && inboxFolder.IsSynchronizationEnabled)
|
||||
{
|
||||
folders.Add(inboxFolder);
|
||||
}
|
||||
}
|
||||
else if (options.Type == SynchronizationType.Full)
|
||||
{
|
||||
// Only get sync enabled folders.
|
||||
|
||||
var synchronizationFolders = await Connection.Table<MailItemFolder>()
|
||||
.Where(a => a.MailAccountId == options.AccountId && a.IsSynchronizationEnabled)
|
||||
.OrderBy(a => a.SpecialFolderType)
|
||||
.ToListAsync();
|
||||
|
||||
folders.AddRange(synchronizationFolders);
|
||||
}
|
||||
else if (options.Type == SynchronizationType.Custom)
|
||||
{
|
||||
// Only get the specified and enabled folders.
|
||||
|
||||
var synchronizationFolders = await Connection.Table<MailItemFolder>()
|
||||
.Where(a => a.MailAccountId == options.AccountId && a.IsSynchronizationEnabled && options.SynchronizationFolderIds.Contains(a.Id))
|
||||
.ToListAsync();
|
||||
|
||||
folders.AddRange(synchronizationFolders);
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
public Task<MailItemFolder> GetFolderAsync(Guid accountId, string remoteFolderId)
|
||||
=> Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.RemoteFolderId == remoteFolderId);
|
||||
|
||||
// v2
|
||||
public async Task BulkUpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders)
|
||||
{
|
||||
var existingFolders = await GetFoldersAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
var foldersToInsert = allFolders.ExceptBy(existingFolders, a => a.RemoteFolderId);
|
||||
var foldersToDelete = existingFolders.ExceptBy(allFolders, a => a.RemoteFolderId);
|
||||
var foldersToUpdate = allFolders.Except(foldersToInsert).Except(foldersToDelete);
|
||||
|
||||
_logger.Debug("Found {0} folders to insert, {1} folders to update and {2} folders to delete.",
|
||||
foldersToInsert.Count(),
|
||||
foldersToUpdate.Count(),
|
||||
foldersToDelete.Count());
|
||||
|
||||
foreach (var folder in foldersToInsert)
|
||||
{
|
||||
await InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in foldersToUpdate)
|
||||
{
|
||||
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in foldersToDelete)
|
||||
{
|
||||
await DeleteFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier)
|
||||
{
|
||||
var folder = await GetFolderAsync(folderId).ConfigureAwait(false);
|
||||
|
||||
if (folder == null)
|
||||
{
|
||||
_logger.Warning("Folder with id {FolderId} does not exist.", folderId);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
folder.DeltaToken = synchronizationIdentifier;
|
||||
|
||||
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
|
||||
return synchronizationIdentifier;
|
||||
}
|
||||
|
||||
public async Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
|
||||
{
|
||||
var folder = await GetFolderAsync(accountId, remoteFolderId);
|
||||
|
||||
if (folder == null)
|
||||
{
|
||||
_logger.Warning("Folder with id {RemoteFolderId} does not exist. Delete folder canceled.", remoteFolderId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await DeleteFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ChangeFolderShowUnreadCountStateAsync(Guid folderId, bool showUnreadCount)
|
||||
{
|
||||
var localFolder = await GetFolderAsync(folderId);
|
||||
|
||||
if (localFolder != null)
|
||||
{
|
||||
localFolder.ShowUnreadCount = showUnreadCount;
|
||||
|
||||
await UpdateFolderAsync(localFolder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Inbox folder is always included for account menu item unread count.
|
||||
public Task<List<MailItemFolder>> GetUnreadUpdateFoldersAsync(Guid accountId)
|
||||
=> Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId && (a.ShowUnreadCount || a.SpecialFolderType == SpecialFolderType.Inbox)).ToListAsync();
|
||||
|
||||
public async Task TestAsync()
|
||||
{
|
||||
var account = new MailAccount()
|
||||
{
|
||||
Address = "test@test.com",
|
||||
ProviderType = MailProviderType.Gmail,
|
||||
Name = "Test Account",
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(account);
|
||||
|
||||
var pref = new MailAccountPreferences
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = account.Id
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(pref);
|
||||
|
||||
ReportUIChange(new AccountCreatedMessage(account));
|
||||
}
|
||||
|
||||
public async Task<bool> IsInboxAvailableForAccountAsync(Guid accountId)
|
||||
=> (await Connection.Table<MailItemFolder>()
|
||||
.Where(a => a.SpecialFolderType == SpecialFolderType.Inbox && a.MailAccountId == accountId)
|
||||
.CountAsync()) == 1;
|
||||
}
|
||||
}
|
||||
50
Wino.Core/Services/FontService.cs
Normal file
50
Wino.Core/Services/FontService.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class FontService : IFontService
|
||||
{
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private ILogger _logger = Log.ForContext<FontService>();
|
||||
|
||||
private readonly List<ReaderFontModel> _availableFonts =
|
||||
[
|
||||
new ReaderFontModel(ReaderFont.Arial, "Arial"),
|
||||
new ReaderFontModel(ReaderFont.Calibri, "Calibri"),
|
||||
new ReaderFontModel(ReaderFont.TimesNewRoman, "Times New Roman"),
|
||||
new ReaderFontModel(ReaderFont.TrebuchetMS, "Trebuchet MS"),
|
||||
new ReaderFontModel(ReaderFont.Tahoma, "Tahoma"),
|
||||
new ReaderFontModel(ReaderFont.Verdana, "Verdana"),
|
||||
new ReaderFontModel(ReaderFont.Georgia, "Georgia"),
|
||||
new ReaderFontModel(ReaderFont.CourierNew, "Courier New")
|
||||
];
|
||||
|
||||
public FontService(IPreferencesService preferencesService)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
|
||||
public List<ReaderFontModel> GetReaderFonts() => _availableFonts;
|
||||
|
||||
public void ChangeReaderFont(ReaderFont font)
|
||||
{
|
||||
_preferencesService.ReaderFont = font;
|
||||
|
||||
_logger.Information("Default reader font is changed to {Font}", font);
|
||||
}
|
||||
|
||||
public void ChangeReaderFontSize(int size)
|
||||
{
|
||||
_preferencesService.ReaderFontSize = size;
|
||||
|
||||
_logger.Information("Default reader font size is changed to {Size}", size);
|
||||
}
|
||||
|
||||
public ReaderFontModel GetCurrentReaderFont() => _availableFonts.Find(f => f.Font == _preferencesService.ReaderFont);
|
||||
public int GetCurrentReaderFontSize() => _preferencesService.ReaderFontSize;
|
||||
}
|
||||
}
|
||||
53
Wino.Core/Services/ImapTestService.cs
Normal file
53
Wino.Core/Services/ImapTestService.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MailKit;
|
||||
using MailKit.Net.Imap;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class ImapTestService : IImapTestService
|
||||
{
|
||||
public const string ProtocolLogFileName = "ImapProtocolLog.log";
|
||||
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IAppInitializerService _appInitializerService;
|
||||
|
||||
public ImapTestService(IPreferencesService preferencesService, IAppInitializerService appInitializerService)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
_appInitializerService = appInitializerService;
|
||||
}
|
||||
|
||||
public async Task TestImapConnectionAsync(CustomServerInformation serverInformation)
|
||||
{
|
||||
ImapClient client = null;
|
||||
|
||||
if (_preferencesService.IsMailkitProtocolLoggerEnabled)
|
||||
{
|
||||
// Create new file for protocol logger.
|
||||
|
||||
var localAppFolderPath = _appInitializerService.GetApplicationDataFolder();
|
||||
|
||||
var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName);
|
||||
|
||||
if (File.Exists(logFile))
|
||||
File.Delete(logFile);
|
||||
|
||||
var stream = File.Create(logFile);
|
||||
|
||||
client = new ImapClient(new ProtocolLogger(stream));
|
||||
}
|
||||
else
|
||||
client = new ImapClient();
|
||||
|
||||
using (client)
|
||||
{
|
||||
// todo: test connection
|
||||
// await client.InitializeAsync(serverInformation);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Wino.Core/Services/LogInitializer.cs
Normal file
41
Wino.Core/Services/LogInitializer.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.IO;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class LogInitializer : ILogInitializer
|
||||
{
|
||||
public const string WinoLogFileName = "WinoDiagnostics.log";
|
||||
|
||||
private readonly LoggingLevelSwitch _levelSwitch = new LoggingLevelSwitch();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
|
||||
public LogInitializer(IPreferencesService preferencesService)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
|
||||
RefreshLoggingLevel();
|
||||
}
|
||||
|
||||
public void RefreshLoggingLevel()
|
||||
{
|
||||
_levelSwitch.MinimumLevel = _preferencesService.IsLoggingEnabled ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Fatal;
|
||||
}
|
||||
|
||||
public void SetupLogger(string logFolderPath)
|
||||
{
|
||||
string logFilePath = Path.Combine(logFolderPath, WinoLogFileName);
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.ControlledBy(_levelSwitch)
|
||||
.WriteTo.File(logFilePath)
|
||||
.WriteTo.Debug()
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.CreateLogger();
|
||||
}
|
||||
}
|
||||
}
|
||||
833
Wino.Core/Services/MailService.cs
Normal file
833
Wino.Core/Services/MailService.cs
Normal file
@@ -0,0 +1,833 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using MimeKit.Text;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class MailService : BaseDatabaseService, IMailService
|
||||
{
|
||||
private const int ItemLoadCount = 20;
|
||||
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ISignatureService _signatureService;
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<MailService>();
|
||||
|
||||
public MailService(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IContactService contactService,
|
||||
IAccountService accountService,
|
||||
ISignatureService signatureService,
|
||||
IThreadingStrategyProvider threadingStrategyProvider,
|
||||
IMimeFileService mimeFileService) : base(databaseService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
_contactService = contactService;
|
||||
_accountService = accountService;
|
||||
_signatureService = signatureService;
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
_mimeFileService = mimeFileService;
|
||||
}
|
||||
|
||||
public async Task<MailCopy> CreateDraftAsync(MailAccount composerAccount,
|
||||
MimeMessage createdDraftMimeMessage,
|
||||
MimeMessage replyingMimeMessage = null,
|
||||
IMailItem replyingMailItem = null)
|
||||
{
|
||||
bool isImapAccount = composerAccount.ServerInformation != null;
|
||||
|
||||
string fromName;
|
||||
|
||||
if (isImapAccount)
|
||||
fromName = composerAccount.ServerInformation.DisplayName;
|
||||
else
|
||||
{
|
||||
var composerContact = await _contactService.GetAddressInformationByAddressAsync(composerAccount.Address);
|
||||
|
||||
fromName = composerContact?.Name ?? composerAccount.Address;
|
||||
}
|
||||
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
|
||||
|
||||
// Get locally created unique id from the mime headers.
|
||||
// This header will be used to map the local draft copy with the remote draft copy.
|
||||
var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader];
|
||||
|
||||
var copy = new MailCopy
|
||||
{
|
||||
UniqueId = Guid.Parse(mimeUniqueId),
|
||||
Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id.
|
||||
CreationDate = DateTime.UtcNow,
|
||||
FromAddress = composerAccount.Address,
|
||||
FromName = fromName,
|
||||
HasAttachments = false,
|
||||
Importance = MailImportance.Normal,
|
||||
Subject = createdDraftMimeMessage.Subject,
|
||||
PreviewText = createdDraftMimeMessage.TextBody,
|
||||
IsRead = true,
|
||||
IsDraft = true,
|
||||
FolderId = draftFolder.Id,
|
||||
DraftId = $"{Constants.LocalDraftStartPrefix}{Guid.NewGuid()}",
|
||||
AssignedFolder = draftFolder,
|
||||
AssignedAccount = composerAccount,
|
||||
FileId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// If replying, add In-Reply-To, ThreadId and References.
|
||||
bool isReplying = replyingMimeMessage != null;
|
||||
|
||||
if (isReplying)
|
||||
{
|
||||
if (replyingMimeMessage.References != null)
|
||||
copy.References = string.Join(",", replyingMimeMessage.References);
|
||||
|
||||
if (!string.IsNullOrEmpty(replyingMimeMessage.MessageId))
|
||||
copy.InReplyTo = replyingMimeMessage.MessageId;
|
||||
|
||||
if (!string.IsNullOrEmpty(replyingMailItem?.ThreadId))
|
||||
copy.ThreadId = replyingMailItem.ThreadId;
|
||||
}
|
||||
|
||||
await Connection.InsertAsync(copy);
|
||||
|
||||
|
||||
await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id);
|
||||
|
||||
ReportUIChange(new DraftCreated(copy, composerAccount));
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public Task<List<string>> GetMailIdsByFolderIdAsync(Guid folderId)
|
||||
=> Connection.QueryScalarsAsync<string>("SELECT Id FROM MailCopy WHERE FolderId = ?", folderId);
|
||||
|
||||
|
||||
private string BuildMailFetchQuery(MailListInitializationOptions options)
|
||||
{
|
||||
// If the search query is there, we should ignore some properties and trim it.
|
||||
//if (!string.IsNullOrEmpty(options.SearchQuery))
|
||||
//{
|
||||
// options.IsFocusedOnly = null;
|
||||
// filterType = FilterOptionType.All;
|
||||
|
||||
// searchQuery = searchQuery.Trim();
|
||||
//}
|
||||
|
||||
// SQLite PCL doesn't support joins.
|
||||
// We make the query using SqlKatka and execute it directly on SQLite-PCL.
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Join("MailItemFolder", "MailCopy.FolderId", "MailItemFolder.Id")
|
||||
.WhereIn("MailCopy.FolderId", options.Folders.Select(a => a.Id))
|
||||
.Take(ItemLoadCount)
|
||||
.SelectRaw("MailCopy.*");
|
||||
|
||||
if (options.SortingOptionType == SortingOptionType.ReceiveDate)
|
||||
query.OrderByDesc("CreationDate");
|
||||
else if (options.SortingOptionType == SortingOptionType.Sender)
|
||||
query.OrderBy("FromName");
|
||||
|
||||
// Conditional where.
|
||||
switch (options.FilterType)
|
||||
{
|
||||
case FilterOptionType.Unread:
|
||||
query.Where("MailCopy.IsRead", false);
|
||||
break;
|
||||
case FilterOptionType.Flagged:
|
||||
query.Where("MailCopy.IsFlagged", true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (options.IsFocusedOnly != null)
|
||||
query.Where("MailCopy.IsFocused", options.IsFocusedOnly.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(options.SearchQuery))
|
||||
query.Where(a =>
|
||||
a.OrWhereContains("MailCopy.PreviewText", options.SearchQuery)
|
||||
.OrWhereContains("MailCopy.Subject", options.SearchQuery)
|
||||
.OrWhereContains("MailCopy.FromName", options.SearchQuery)
|
||||
.OrWhereContains("MailCopy.FromAddress", options.SearchQuery));
|
||||
|
||||
if (options.ExistingUniqueIds?.Any() ?? false)
|
||||
{
|
||||
query.WhereNotIn("MailCopy.UniqueId", options.ExistingUniqueIds);
|
||||
}
|
||||
|
||||
//if (options.Skip > 0)
|
||||
//{
|
||||
// query.Skip(options.Skip);
|
||||
//}
|
||||
|
||||
return query.GetRawQuery();
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options)
|
||||
{
|
||||
var query = BuildMailFetchQuery(options);
|
||||
|
||||
var mails = await Connection.QueryAsync<MailCopy>(query);
|
||||
|
||||
// Fill in assigned account and folder for each mail.
|
||||
// To speed things up a bit, we'll load account and assigned folder in groups
|
||||
// to reduce the query time.
|
||||
|
||||
var groupedByFolders = mails.GroupBy(a => a.FolderId);
|
||||
|
||||
foreach (var group in groupedByFolders)
|
||||
{
|
||||
MailItemFolder folderAssignment = null;
|
||||
MailAccount accountAssignment = null;
|
||||
|
||||
folderAssignment = await _folderService.GetFolderAsync(group.Key).ConfigureAwait(false);
|
||||
|
||||
if (folderAssignment != null)
|
||||
{
|
||||
accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
group.ForEach(a =>
|
||||
{
|
||||
a.AssignedFolder = folderAssignment;
|
||||
a.AssignedAccount = accountAssignment;
|
||||
});
|
||||
}
|
||||
|
||||
// Remove items that has no assigned account or folder.
|
||||
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
|
||||
|
||||
// Each account items must be threaded separately.
|
||||
|
||||
if (options.CreateThreads)
|
||||
{
|
||||
var threadedItems = new List<IMailItem>();
|
||||
|
||||
var groupedByAccounts = mails.GroupBy(a => a.AssignedAccount.Id);
|
||||
|
||||
foreach (var group in groupedByAccounts)
|
||||
{
|
||||
if (!group.Any()) continue;
|
||||
|
||||
var accountId = group.Key;
|
||||
var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
|
||||
|
||||
var threadingStrategy = _threadingStrategyProvider.GetStrategy(groupAccount.ProviderType);
|
||||
|
||||
// Only thread items from Draft and Sent folders must present here.
|
||||
// Otherwise this strategy will fetch the items that are in Deleted folder as well.
|
||||
var accountThreadedItems = await threadingStrategy.ThreadItemsAsync(group.ToList());
|
||||
|
||||
if (accountThreadedItems != null)
|
||||
{
|
||||
threadedItems.AddRange(accountThreadedItems);
|
||||
}
|
||||
}
|
||||
|
||||
threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||
|
||||
return threadedItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Threading is disabled. Just return everything as it is.
|
||||
|
||||
mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||
|
||||
return new List<IMailItem>(mails);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<MailCopy>> GetMailItemsAsync(string mailCopyId)
|
||||
{
|
||||
var mailCopies = await Connection.Table<MailCopy>().Where(a => a.Id == mailCopyId).ToListAsync();
|
||||
|
||||
foreach (var mailCopy in mailCopies)
|
||||
{
|
||||
await LoadAssignedPropertiesAsync(mailCopy).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return mailCopies;
|
||||
}
|
||||
|
||||
private async Task LoadAssignedPropertiesAsync(MailCopy mailCopy)
|
||||
{
|
||||
if (mailCopy == null) return;
|
||||
|
||||
// Load AssignedAccount and AssignedFolder.
|
||||
|
||||
var folder = await _folderService.GetFolderAsync(mailCopy.FolderId);
|
||||
|
||||
if (folder == null) return;
|
||||
|
||||
var account = await _accountService.GetAccountAsync(folder.MailAccountId);
|
||||
|
||||
if (account == null) return;
|
||||
|
||||
mailCopy.AssignedAccount = account;
|
||||
mailCopy.AssignedFolder = folder;
|
||||
}
|
||||
|
||||
public async Task<MailCopy> GetSingleMailItemWithoutFolderAssignmentAsync(string mailCopyId)
|
||||
{
|
||||
var mailCopy = await Connection.Table<MailCopy>().FirstOrDefaultAsync(a => a.Id == mailCopyId);
|
||||
|
||||
if (mailCopy == null) return null;
|
||||
|
||||
await LoadAssignedPropertiesAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
return mailCopy;
|
||||
}
|
||||
|
||||
public async Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId)
|
||||
{
|
||||
var query = new Query("MailCopy")
|
||||
.Join("MailItemFolder", "MailCopy.FolderId", "MailItemFolder.Id")
|
||||
.Where("MailCopy.Id", mailCopyId)
|
||||
.Where("MailItemFolder.RemoteFolderId", remoteFolderId)
|
||||
.SelectRaw("MailCopy.*")
|
||||
.GetRawQuery();
|
||||
|
||||
var mailItem = await Connection.FindWithQueryAsync<MailCopy>(query);
|
||||
|
||||
if (mailItem == null) return null;
|
||||
|
||||
await LoadAssignedPropertiesAsync(mailItem).ConfigureAwait(false);
|
||||
|
||||
return mailItem;
|
||||
}
|
||||
|
||||
public async Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId)
|
||||
{
|
||||
var mailItem = await Connection.FindAsync<MailCopy>(uniqueMailId);
|
||||
|
||||
if (mailItem == null) return null;
|
||||
|
||||
await LoadAssignedPropertiesAsync(mailItem).ConfigureAwait(false);
|
||||
|
||||
return mailItem;
|
||||
}
|
||||
|
||||
// v2
|
||||
|
||||
public async Task DeleteMailAsync(Guid accountId, string mailCopyId)
|
||||
{
|
||||
var allMails = await GetMailItemsAsync(mailCopyId).ConfigureAwait(false);
|
||||
|
||||
foreach (var mailItem in allMails)
|
||||
{
|
||||
await DeleteMailInternalAsync(mailItem).ConfigureAwait(false);
|
||||
|
||||
// Delete mime file.
|
||||
// Even though Gmail might have multiple copies for the same mail, we only have one MIME file for all.
|
||||
// Their FileId is inserted same.
|
||||
await _mimeFileService.DeleteMimeMessageAsync(accountId, mailItem.FileId);
|
||||
}
|
||||
}
|
||||
|
||||
#region Repository Calls
|
||||
|
||||
private async Task InsertMailAsync(MailCopy mailCopy)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Null mail passed to InsertMailAsync call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mailCopy.FolderId == Guid.Empty)
|
||||
{
|
||||
_logger.Warning("Invalid FolderId for MailCopyId {Id} for InsertMailAsync", mailCopy.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Inserting mail {MailCopyId} to Folder {FolderId}", mailCopy.Id, mailCopy.FolderId);
|
||||
|
||||
await Connection.InsertAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new MailAddedMessage(mailCopy));
|
||||
}
|
||||
|
||||
public async Task UpdateMailAsync(MailCopy mailCopy)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Null mail passed to UpdateMailAsync call.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Updating mail {MailCopyId} with Folder {FolderId}", mailCopy.Id, mailCopy.FolderId);
|
||||
|
||||
await Connection.UpdateAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new MailUpdatedMessage(mailCopy));
|
||||
}
|
||||
|
||||
private async Task DeleteMailInternalAsync(MailCopy mailCopy)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Null mail passed to DeleteMailAsync call.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Deleting mail {Id} with Folder {FolderId}", mailCopy.Id, mailCopy.FolderId);
|
||||
|
||||
await Connection.DeleteAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new MailRemovedMessage(mailCopy));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task UpdateAllMailCopiesAsync(string mailCopyId, Func<MailCopy, bool> action)
|
||||
{
|
||||
var mailCopies = await GetMailItemsAsync(mailCopyId);
|
||||
|
||||
if (mailCopies == null || !mailCopies.Any())
|
||||
{
|
||||
_logger.Warning("Updating mail copies failed because there are no copies available with Id {MailCopyId}", mailCopyId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Information("Updating {MailCopyCount} mail copies with Id {MailCopyId}", mailCopies.Count, mailCopyId);
|
||||
|
||||
foreach (var mailCopy in mailCopies)
|
||||
{
|
||||
bool shouldUpdateItem = action(mailCopy);
|
||||
|
||||
if (shouldUpdateItem)
|
||||
{
|
||||
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
_logger.Information("Skipped updating mail because it is already in the desired state.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task ChangeReadStatusAsync(string mailCopyId, bool isRead)
|
||||
=> UpdateAllMailCopiesAsync(mailCopyId, (item) =>
|
||||
{
|
||||
item.IsRead = isRead;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
|
||||
=> UpdateAllMailCopiesAsync(mailCopyId, (item) =>
|
||||
{
|
||||
item.IsFlagged = isFlagged;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
public async Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||
{
|
||||
// Note: Folder might not be available at the moment due to user not syncing folders before the delta processing.
|
||||
// This is a problem, because assignments won't be created.
|
||||
// Therefore we sync folders every time before the delta processing.
|
||||
|
||||
var localFolder = await _folderService.GetFolderAsync(accountId, remoteFolderId);
|
||||
|
||||
if (localFolder == null)
|
||||
{
|
||||
_logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId);
|
||||
_logger.Warning("Skipping assignment creation for the the message {MailCopyId}", mailCopyId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var mailCopy = await GetSingleMailItemWithoutFolderAssignmentAsync(mailCopyId);
|
||||
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Can't create assignment for mail {MailCopyId} because it does not exist.", mailCopyId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy one of the mail copy and assign it to the new folder.
|
||||
// We don't need to create a new MIME pack.
|
||||
// Therefore FileId is not changed for the new MailCopy.
|
||||
|
||||
mailCopy.UniqueId = Guid.NewGuid();
|
||||
mailCopy.FolderId = localFolder.Id;
|
||||
mailCopy.AssignedFolder = localFolder;
|
||||
|
||||
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||
{
|
||||
var mailItem = await GetSingleMailItemAsync(mailCopyId, remoteFolderId).ConfigureAwait(false);
|
||||
|
||||
if (mailItem == null)
|
||||
{
|
||||
_logger.Warning("Mail not found with id {MailCopyId} with remote folder {RemoteFolderId}", mailCopyId, remoteFolderId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var localFolder = await _folderService.GetFolderAsync(accountId, remoteFolderId);
|
||||
|
||||
if (localFolder == null)
|
||||
{
|
||||
_logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await DeleteMailInternalAsync(mailItem).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
|
||||
{
|
||||
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
if (account == null) return false;
|
||||
|
||||
if (string.IsNullOrEmpty(package.AssignedRemoteFolderId))
|
||||
{
|
||||
_logger.Warning("Remote folder id is not set for {MailCopyId}.", package.Copy.Id);
|
||||
_logger.Warning("Ignoring creation of mail.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var assignedFolder = await _folderService.GetFolderAsync(accountId, package.AssignedRemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
if (assignedFolder == null)
|
||||
{
|
||||
_logger.Warning("Assigned folder not found for {MailCopyId}.", package.Copy.Id);
|
||||
_logger.Warning("Ignoring creation of mail.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var mailCopy = package.Copy;
|
||||
var mimeMessage = package.Mime;
|
||||
|
||||
mailCopy.UniqueId = Guid.NewGuid();
|
||||
mailCopy.AssignedAccount = account;
|
||||
mailCopy.AssignedFolder = assignedFolder;
|
||||
mailCopy.FolderId = assignedFolder.Id;
|
||||
|
||||
// Only save MIME files if they don't exists.
|
||||
// This is because 1 mail may have multiple copies in different folders.
|
||||
// but only single MIME to represent all.
|
||||
|
||||
// Save mime file to disk.
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(accountId, mailCopy.FileId);
|
||||
|
||||
if (!isMimeExists)
|
||||
{
|
||||
bool isMimeSaved = await _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, accountId).ConfigureAwait(false);
|
||||
|
||||
if (!isMimeSaved)
|
||||
{
|
||||
_logger.Warning("Failed to save mime file for {MailCopyId}.", mailCopy.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Save contact information.
|
||||
await _contactService.SaveAddressInformationAsync(mimeMessage).ConfigureAwait(false);
|
||||
|
||||
// Create mail copy in the database.
|
||||
// Update if exists.
|
||||
|
||||
var existingCopyItem = await Connection.Table<MailCopy>()
|
||||
.FirstOrDefaultAsync(a => a.Id == mailCopy.Id && a.FolderId == assignedFolder.Id);
|
||||
|
||||
if (existingCopyItem != null)
|
||||
{
|
||||
mailCopy.UniqueId = existingCopyItem.UniqueId;
|
||||
|
||||
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MimeMessage> CreateDraftMimeMessageAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||
{
|
||||
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
|
||||
// Same unique id will be used for the local copy as well.
|
||||
// Synchronizer will map this unique id to the local draft copy after synchronization.
|
||||
|
||||
var messageUniqueId = Guid.NewGuid();
|
||||
|
||||
var message = new MimeMessage()
|
||||
{
|
||||
Headers = { { Constants.WinoLocalDraftHeader, messageUniqueId.ToString() } }
|
||||
};
|
||||
|
||||
var builder = new BodyBuilder();
|
||||
|
||||
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
_logger.Warning("Can't create draft mime message because account {AccountId} does not exist.", accountId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var reason = draftCreationOptions.Reason;
|
||||
var referenceMessage = draftCreationOptions.ReferenceMimeMessage;
|
||||
|
||||
// For API synchronizers we should get this from contacts.
|
||||
if (account.ServerInformation == null)
|
||||
{
|
||||
var fromContact = await _contactService.GetAddressInformationByAddressAsync(account.Address).ConfigureAwait(false)
|
||||
?? new AddressInformation() { Name = account.Address, Address = account.Address };
|
||||
|
||||
message.From.Add(new MailboxAddress(fromContact.Name, fromContact.Address));
|
||||
}
|
||||
else
|
||||
{
|
||||
// For IMAP synchronizer, we have already Display Name in the settings.
|
||||
message.From.Add(new MailboxAddress(account.ServerInformation.DisplayName, account.ServerInformation.Address));
|
||||
}
|
||||
|
||||
// Manage "To"
|
||||
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll)
|
||||
{
|
||||
// Reply to the sender of the message
|
||||
|
||||
if (referenceMessage.ReplyTo.Count > 0)
|
||||
message.To.AddRange(referenceMessage.ReplyTo);
|
||||
else if (referenceMessage.From.Count > 0)
|
||||
message.To.AddRange(referenceMessage.From);
|
||||
else if (referenceMessage.Sender != null)
|
||||
message.To.Add(referenceMessage.Sender);
|
||||
|
||||
if (reason == DraftCreationReason.ReplyAll)
|
||||
{
|
||||
// Include all of the other original recipients
|
||||
message.To.AddRange(referenceMessage.To);
|
||||
|
||||
// Find self and remove
|
||||
var self = message.To.FirstOrDefault(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == account.Address);
|
||||
|
||||
if (self != null)
|
||||
message.To.Remove(self);
|
||||
|
||||
message.Cc.AddRange(referenceMessage.Cc);
|
||||
}
|
||||
|
||||
// Manage "ThreadId-ConversationId"
|
||||
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
|
||||
{
|
||||
message.InReplyTo = referenceMessage.MessageId;
|
||||
|
||||
foreach (var id in referenceMessage.References)
|
||||
message.References.Add(id);
|
||||
|
||||
message.References.Add(referenceMessage.MessageId);
|
||||
}
|
||||
|
||||
message.Headers.Add("Thread-Topic", referenceMessage.Subject);
|
||||
}
|
||||
|
||||
var previewer = new HtmlTextPreviewer();
|
||||
|
||||
if (reason == DraftCreationReason.Forward)
|
||||
{
|
||||
var visitor = _mimeFileService.CreateHTMLPreviewVisitor(referenceMessage, string.Empty);
|
||||
visitor.Visit(referenceMessage);
|
||||
|
||||
builder.HtmlBody = visitor.HtmlBody;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add signature if any.
|
||||
var accountSignature = await _signatureService.GetAccountSignatureAsync(account.Id);
|
||||
|
||||
if (accountSignature != null)
|
||||
{
|
||||
// Leave some space for new mail content.
|
||||
|
||||
builder.HtmlBody = @$"<html><br><br>{accountSignature.HtmlBody}</html>";
|
||||
}
|
||||
}
|
||||
|
||||
// Manage Subject
|
||||
if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase))
|
||||
message.Subject = $"FW: {referenceMessage.Subject}";
|
||||
else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) &&
|
||||
!referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase))
|
||||
message.Subject = $"RE: {referenceMessage.Subject}";
|
||||
else if (referenceMessage != null)
|
||||
message.Subject = referenceMessage.Subject;
|
||||
|
||||
// Only include attachments if forwarding.
|
||||
if (reason == DraftCreationReason.Forward && (referenceMessage?.Attachments?.Any() ?? false))
|
||||
{
|
||||
foreach (var attachment in referenceMessage.Attachments)
|
||||
{
|
||||
builder.Attachments.Add(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(builder.HtmlBody))
|
||||
{
|
||||
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
|
||||
}
|
||||
|
||||
message.Body = builder.ToMessageBody();
|
||||
|
||||
// Apply mail-to protocol parameters if exists.
|
||||
|
||||
if (draftCreationOptions.MailtoParameters != null)
|
||||
{
|
||||
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoSubjectParameterKey, out string subjectParameter))
|
||||
message.Subject = subjectParameter;
|
||||
|
||||
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBodyParameterKey, out string bodyParameter))
|
||||
{
|
||||
builder.TextBody = bodyParameter;
|
||||
builder.HtmlBody = bodyParameter;
|
||||
|
||||
message.Body = builder.ToMessageBody();
|
||||
}
|
||||
|
||||
InternetAddressList ExtractRecipients(string parameterValue)
|
||||
{
|
||||
var list = new InternetAddressList();
|
||||
|
||||
var splittedRecipients = parameterValue.Split(',');
|
||||
|
||||
foreach (var recipient in splittedRecipients)
|
||||
list.Add(new MailboxAddress(recipient, recipient));
|
||||
|
||||
return list;
|
||||
|
||||
}
|
||||
|
||||
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoToParameterKey, out string toParameter))
|
||||
message.To.AddRange(ExtractRecipients(toParameter));
|
||||
|
||||
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoCCParameterKey, out string ccParameter))
|
||||
message.Cc.AddRange(ExtractRecipients(ccParameter));
|
||||
|
||||
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBCCParameterKey, out string bccParameter))
|
||||
message.Bcc.AddRange(ExtractRecipients(bccParameter));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update TextBody from existing HtmlBody if exists.
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
|
||||
{
|
||||
var query = new Query("MailCopy")
|
||||
.Join("MailItemFolder", "MailCopy.FolderId", "MailItemFolder.Id")
|
||||
.Where("MailCopy.UniqueId", localDraftCopyUniqueId)
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.SelectRaw("MailCopy.*")
|
||||
.GetRawQuery();
|
||||
|
||||
var localDraftCopy = await Connection.FindWithQueryAsync<MailCopy>(query);
|
||||
|
||||
if (localDraftCopy == null)
|
||||
{
|
||||
_logger.Warning("Draft mapping failed because local draft copy with unique id {LocalDraftCopyUniqueId} does not exist.", localDraftCopyUniqueId);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldLocalDraftId = localDraftCopy.Id;
|
||||
|
||||
await LoadAssignedPropertiesAsync(localDraftCopy).ConfigureAwait(false);
|
||||
|
||||
bool isIdChanging = localDraftCopy.Id != newMailCopyId;
|
||||
|
||||
localDraftCopy.Id = newMailCopyId;
|
||||
localDraftCopy.DraftId = newDraftId;
|
||||
localDraftCopy.ThreadId = newThreadId;
|
||||
|
||||
await UpdateMailAsync(localDraftCopy).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new DraftMapped(oldLocalDraftId, newDraftId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId)
|
||||
{
|
||||
return UpdateAllMailCopiesAsync(mailCopyId, (item) =>
|
||||
{
|
||||
if (item.ThreadId != newThreadId || item.DraftId != newDraftId)
|
||||
{
|
||||
var oldDraftId = item.DraftId;
|
||||
|
||||
item.DraftId = newDraftId;
|
||||
item.ThreadId = newThreadId;
|
||||
|
||||
ReportUIChange(new DraftMapped(oldDraftId, newDraftId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds)
|
||||
{
|
||||
var rawQuery = new Query("MailCopy")
|
||||
.Join("MailItemFolder", "MailCopy.FolderId", "MailItemFolder.Id")
|
||||
.WhereIn("MailCopy.Id", downloadedMailCopyIds)
|
||||
.Where("MailCopy.IsRead", false)
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.Where("MailItemFolder.SpecialFolderType", SpecialFolderType.Inbox)
|
||||
.SelectRaw("MailCopy.*")
|
||||
.GetRawQuery();
|
||||
|
||||
return Connection.QueryAsync<MailCopy>(rawQuery);
|
||||
}
|
||||
|
||||
public Task<MailAccount> GetMailAccountByUniqueIdAsync(Guid uniqueMailId)
|
||||
{
|
||||
var query = new Query("MailCopy")
|
||||
.Join("MailItemFolder", "MailCopy.FolderId", "MailItemFolder.Id")
|
||||
.Join("MailAccount", "MailItemFolder.MailAccountId", "MailAccount.Id")
|
||||
.Where("MailCopy.UniqueId", uniqueMailId)
|
||||
.SelectRaw("MailAccount.*")
|
||||
.GetRawQuery();
|
||||
|
||||
return Connection.FindWithQueryAsync<MailAccount>(query);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
242
Wino.Core/Services/MimeFileService.cs
Normal file
242
Wino.Core/Services/MimeFileService.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Mime;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public interface IMimeFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds the EML file for the given mail id for address, parses and returns MimeMessage.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Mime message information</returns>
|
||||
Task<MimeMessageInformation> GetMimeMessageInformationAsync(Guid fileId, Guid accountId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mime message information for the given EML file bytes.
|
||||
/// This override is used when EML file association launch is used
|
||||
/// because we may not have the access to the file path.
|
||||
/// </summary>
|
||||
/// <param name="fileBytes">Byte array of the file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Mime message information</returns>
|
||||
Task<MimeMessageInformation> GetMimeMessageInformationAsync(byte[] fileBytes, string emlFilePath, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves EML file to the disk.
|
||||
/// </summary>
|
||||
/// <param name="copy">MailCopy of the native message.</param>
|
||||
/// <param name="mimeMessage">MimeMessage that is parsed from native message.</param>
|
||||
/// <param name="accountId">Which account Id to save this file for.</param>
|
||||
Task<bool> SaveMimeMessageAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path that all Mime resources (including eml) is stored for this MailCopyId
|
||||
/// This is useful for storing previously rendered attachments as well.
|
||||
/// </summary>
|
||||
/// <param name="accountAddress">Account address</param>
|
||||
/// <param name="mailCopyId">Resource mail copy id</param>
|
||||
Task<string> GetMimeResourcePathAsync(Guid accountId, Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether mime file exists locally or not.
|
||||
/// </summary>
|
||||
Task<bool> IsMimeExistAsync(Guid accountId, Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates HtmlPreviewVisitor for the given MimeMessage.
|
||||
/// </summary>
|
||||
/// <param name="message">Mime</param>
|
||||
/// <param name="mimeLocalPath">File path that mime is located to load resources.</param>
|
||||
HtmlPreviewVisitor CreateHTMLPreviewVisitor(MimeMessage message, string mimeLocalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the given mime file from the disk.
|
||||
/// </summary>
|
||||
Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the final model containing rendering details.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to render.</param>
|
||||
/// <param name="mimeLocalPath">File path that physical MimeMessage is located.</param>
|
||||
/// <param name="options">Rendering options</param>
|
||||
MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null);
|
||||
}
|
||||
|
||||
public class MimeFileService : IMimeFileService
|
||||
{
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private ILogger _logger = Log.ForContext<MimeFileService>();
|
||||
|
||||
public MimeFileService(INativeAppService nativeAppService)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(Guid fileId, Guid accountId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
var mimeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(mimeFilePath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new MimeMessageInformation(loadedMimeMessage, resourcePath);
|
||||
}
|
||||
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(byte[] fileBytes, string emlDirectoryPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var memoryStream = new MemoryStream(fileBytes);
|
||||
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||
return new MimeMessageInformation(loadedMimeMessage, emlDirectoryPath);
|
||||
}
|
||||
|
||||
public async Task<bool> SaveMimeMessageAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
var fileStream = File.Create(completeFilePath);
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not save mime file for FileId: {FileId}", fileId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetEMLPath(string resourcePath) => $"{resourcePath}\\mail.eml";
|
||||
|
||||
public async Task<string> GetMimeResourcePathAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var mimeFolderPath = await _nativeAppService.GetMimeMessageStoragePath().ConfigureAwait(false);
|
||||
var mimeDirectory = Path.Combine(mimeFolderPath, accountId.ToString(), fileId.ToString());
|
||||
|
||||
if (!Directory.Exists(mimeDirectory))
|
||||
Directory.CreateDirectory(mimeDirectory);
|
||||
|
||||
return mimeDirectory;
|
||||
}
|
||||
|
||||
public async Task<bool> IsMimeExistAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
return File.Exists(completeFilePath);
|
||||
}
|
||||
|
||||
public HtmlPreviewVisitor CreateHTMLPreviewVisitor(MimeMessage message, string mimeLocalPath)
|
||||
{
|
||||
var visitor = new HtmlPreviewVisitor(mimeLocalPath);
|
||||
|
||||
message.Accept(visitor);
|
||||
|
||||
// TODO: Match cid with attachments if any.
|
||||
|
||||
return visitor;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
if (File.Exists(completeFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(completeFilePath);
|
||||
|
||||
_logger.Information("Mime file deleted for {FileId}", fileId);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not delete mime file for {FileId}", fileId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||
{
|
||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||
|
||||
string finalRenderHtml = visitor.HtmlBody;
|
||||
|
||||
// Check whether we need to purify the generated HTML from visitor.
|
||||
// No need to create HtmlDocument if not required.
|
||||
|
||||
if (options != null && options.IsPurifyingNeeded())
|
||||
{
|
||||
var document = new HtmlAgilityPack.HtmlDocument();
|
||||
document.LoadHtml(visitor.HtmlBody);
|
||||
|
||||
// Clear <img> src attribute.
|
||||
|
||||
if (!options.LoadImages)
|
||||
document.ClearImages();
|
||||
|
||||
if (!options.LoadStyles)
|
||||
document.ClearStyles();
|
||||
|
||||
// Update final HTML.
|
||||
finalRenderHtml = document.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
var renderingModel = new MailRenderModel(finalRenderHtml, options);
|
||||
|
||||
// Create attachments.
|
||||
|
||||
foreach (var attachment in visitor.Attachments)
|
||||
{
|
||||
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
||||
{
|
||||
renderingModel.Attachments.Add(attachmentPart);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for List-Unsubscribe link if possible.
|
||||
|
||||
if (message.Headers.Contains(HeaderId.ListUnsubscribe))
|
||||
{
|
||||
renderingModel.UnsubscribeLink = message.Headers[HeaderId.ListUnsubscribe].Normalize();
|
||||
|
||||
// Sometimes this link is wrapped with < >, remove them.
|
||||
if (renderingModel.UnsubscribeLink.StartsWith("<"))
|
||||
{
|
||||
renderingModel.UnsubscribeLink = renderingModel.UnsubscribeLink.Substring(1, renderingModel.UnsubscribeLink.Length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
return renderingModel;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
40
Wino.Core/Services/ProviderService.cs
Normal file
40
Wino.Core/Services/ProviderService.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service that is returning available provider details.
|
||||
/// </summary>
|
||||
public class ProviderService : IProviderService
|
||||
{
|
||||
public IProviderDetail GetProviderDetail(MailProviderType type)
|
||||
{
|
||||
var details = GetProviderDetails();
|
||||
|
||||
return details.FirstOrDefault(a => a.Type == type);
|
||||
}
|
||||
|
||||
public List<IProviderDetail> GetProviderDetails()
|
||||
{
|
||||
var providerList = new List<IProviderDetail>();
|
||||
|
||||
var providers = new MailProviderType[]
|
||||
{
|
||||
MailProviderType.Outlook,
|
||||
MailProviderType.Gmail,
|
||||
MailProviderType.IMAP4
|
||||
};
|
||||
|
||||
foreach (var type in providers)
|
||||
{
|
||||
providerList.Add(new ProviderDetail(type));
|
||||
}
|
||||
|
||||
return providerList;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
Wino.Core/Services/SignatureService.cs
Normal file
91
Wino.Core/Services/SignatureService.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class SignatureService : BaseDatabaseService, ISignatureService
|
||||
{
|
||||
public SignatureService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public async Task<AccountSignature> CreateDefaultSignatureAsync(Guid accountId)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
var defaultSignature = GetDefaultSignature();
|
||||
|
||||
await Connection.InsertAsync(defaultSignature);
|
||||
|
||||
account.SignatureId = defaultSignature.Id;
|
||||
|
||||
await Connection.UpdateAsync(account);
|
||||
|
||||
return defaultSignature;
|
||||
}
|
||||
|
||||
public async Task DeleteAccountSignatureAssignment(Guid accountId)
|
||||
{
|
||||
var existingSignature = await GetAccountSignatureAsync(accountId);
|
||||
|
||||
if (existingSignature != null)
|
||||
{
|
||||
await Connection.DeleteAsync(existingSignature);
|
||||
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
account.SignatureId = null;
|
||||
|
||||
await Connection.UpdateAsync(account);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> GetAccountSignatureAsync(Guid accountId)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (account?.SignatureId == null)
|
||||
return null;
|
||||
|
||||
return await Connection.Table<AccountSignature>().FirstOrDefaultAsync(a => a.Id == account.SignatureId);
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> UpdateAccountSignatureAsync(Guid accountId, string htmlBody)
|
||||
{
|
||||
var signature = await GetAccountSignatureAsync(accountId);
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (signature == null)
|
||||
{
|
||||
signature = new AccountSignature()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
HtmlBody = htmlBody
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(signature);
|
||||
}
|
||||
else
|
||||
{
|
||||
signature.HtmlBody = htmlBody;
|
||||
|
||||
await Connection.UpdateAsync(signature);
|
||||
}
|
||||
|
||||
account.SignatureId = signature.Id;
|
||||
|
||||
await Connection.UpdateAsync(account);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
private AccountSignature GetDefaultSignature()
|
||||
{
|
||||
return new AccountSignature()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
HtmlBody = @"<p>Sent from <a href=""https://github.com/bkaankose/Wino-Mail/"">Wino Mail</a> for Windows</p>"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Wino.Core/Services/ThreadingStrategyProvider.cs
Normal file
32
Wino.Core/Services/ThreadingStrategyProvider.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration.Threading;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class ThreadingStrategyProvider : IThreadingStrategyProvider
|
||||
{
|
||||
private readonly OutlookThreadingStrategy _outlookThreadingStrategy;
|
||||
private readonly GmailThreadingStrategy _gmailThreadingStrategy;
|
||||
private readonly ImapThreadStrategy _imapThreadStrategy;
|
||||
|
||||
public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy,
|
||||
GmailThreadingStrategy gmailThreadingStrategy,
|
||||
ImapThreadStrategy imapThreadStrategy)
|
||||
{
|
||||
_outlookThreadingStrategy = outlookThreadingStrategy;
|
||||
_gmailThreadingStrategy = gmailThreadingStrategy;
|
||||
_imapThreadStrategy = imapThreadStrategy;
|
||||
}
|
||||
|
||||
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
|
||||
{
|
||||
return mailProviderType switch
|
||||
{
|
||||
MailProviderType.Outlook or MailProviderType.Office365 => _outlookThreadingStrategy,
|
||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||
_ => _imapThreadStrategy,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Wino.Core/Services/ThumbnailService.cs
Normal file
64
Wino.Core/Services/ThumbnailService.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public static class ThumbnailService
|
||||
{
|
||||
private static string[] knownCompanies = new string[]
|
||||
{
|
||||
"microsoft.com", "apple.com", "google.com", "steampowered.com", "airbnb.com", "youtube.com", "uber.com"
|
||||
};
|
||||
|
||||
public static bool IsKnown(string mailHost) => !string.IsNullOrEmpty(mailHost) && knownCompanies.Contains(mailHost);
|
||||
|
||||
public static string GetHost(string address)
|
||||
{
|
||||
if (string.IsNullOrEmpty(address))
|
||||
return string.Empty;
|
||||
|
||||
if (address.Contains('@'))
|
||||
{
|
||||
var splitted = address.Split('@');
|
||||
|
||||
if (splitted.Length >= 2 && !string.IsNullOrEmpty(splitted[1]))
|
||||
{
|
||||
try
|
||||
{
|
||||
return new MailAddress(address).Host;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// TODO: Exceptions are ignored for now.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static Tuple<bool, string> CheckIsKnown(string host)
|
||||
{
|
||||
// Check known hosts.
|
||||
// Apply company logo if available.
|
||||
|
||||
try
|
||||
{
|
||||
var last = host.Split('.');
|
||||
|
||||
if (last.Length > 2)
|
||||
host = $"{last[last.Length - 2]}.{last[last.Length - 1]}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new Tuple<bool, string>(false, host);
|
||||
}
|
||||
|
||||
return new Tuple<bool, string>(ThumbnailService.IsKnown(host), host);
|
||||
}
|
||||
|
||||
public static string GetKnownHostImage(string host)
|
||||
=> $"ms-appx:///Assets/Thumbnails/{host}.png";
|
||||
}
|
||||
}
|
||||
31
Wino.Core/Services/TokenService.cs
Normal file
31
Wino.Core/Services/TokenService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
Task<TokenInformation> GetTokenInformationAsync(Guid accountId);
|
||||
Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation);
|
||||
}
|
||||
|
||||
public class TokenService : BaseDatabaseService, ITokenService
|
||||
{
|
||||
public TokenService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public Task<TokenInformation> GetTokenInformationAsync(Guid accountId)
|
||||
=> Connection.Table<TokenInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
||||
|
||||
public async Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation)
|
||||
{
|
||||
// Delete all tokens for this account.
|
||||
await Connection.Table<TokenInformation>().DeleteAsync(a => a.AccountId == accountId);
|
||||
|
||||
// Save new token info to the account.
|
||||
tokenInformation.AccountId = accountId;
|
||||
|
||||
await Connection.InsertOrReplaceAsync(tokenInformation);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Wino.Core/Services/TranslationService.cs
Normal file
84
Wino.Core/Services/TranslationService.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
using Wino.Core.Messages.Shell;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class TranslationService : ITranslationService
|
||||
{
|
||||
public const AppLanguage DefaultAppLanguage = AppLanguage.English;
|
||||
|
||||
private ILogger _logger = Log.ForContext<TranslationService>();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private bool isInitialized = false;
|
||||
|
||||
public TranslationService(IPreferencesService preferencesService)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
|
||||
// Initialize default language with ignoring current language check.
|
||||
public Task InitializeAsync() => InitializeLanguageAsync(_preferencesService.CurrentLanguage, ignoreCurrentLanguageCheck: true);
|
||||
|
||||
public async Task InitializeLanguageAsync(AppLanguage language, bool ignoreCurrentLanguageCheck = false)
|
||||
{
|
||||
if (!ignoreCurrentLanguageCheck && _preferencesService.CurrentLanguage == language)
|
||||
{
|
||||
_logger.Warning("Changing language is ignored because current language and requested language are same.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreCurrentLanguageCheck && isInitialized) return;
|
||||
|
||||
var currentDictionary = Translator.Resources;
|
||||
using var resourceStream = currentDictionary.GetLanguageStream(language);
|
||||
|
||||
var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var translationLookups = JsonConvert.DeserializeObject<Dictionary<string, string>>(stremValue);
|
||||
|
||||
// Insert new translation key-value pairs.
|
||||
// Overwrite existing values for the same keys.
|
||||
|
||||
foreach (var pair in translationLookups)
|
||||
{
|
||||
// Replace existing value.
|
||||
if (currentDictionary.ContainsKey(pair.Key))
|
||||
{
|
||||
currentDictionary[pair.Key] = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentDictionary.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
_preferencesService.CurrentLanguage = language;
|
||||
|
||||
isInitialized = true;
|
||||
WeakReferenceMessenger.Default.Send(new LanguageChanged());
|
||||
}
|
||||
|
||||
public List<AppLanguageModel> GetAvailableLanguages()
|
||||
=>
|
||||
[
|
||||
new AppLanguageModel(AppLanguage.English, "English"),
|
||||
new AppLanguageModel(AppLanguage.Deutsch, "Deutsch"),
|
||||
new AppLanguageModel(AppLanguage.Russian, "Russian"),
|
||||
new AppLanguageModel(AppLanguage.Turkish, "Türkçe"),
|
||||
new AppLanguageModel(AppLanguage.Polish, "Polski"),
|
||||
new AppLanguageModel(AppLanguage.Czech, "Czech"),
|
||||
new AppLanguageModel(AppLanguage.Spanish, "Spanish"),
|
||||
new AppLanguageModel(AppLanguage.French, "French"),
|
||||
];
|
||||
}
|
||||
}
|
||||
154
Wino.Core/Services/WinoRequestDelegator.cs
Normal file
154
Wino.Core/Services/WinoRequestDelegator.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Messages.Synchronization;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public class WinoRequestDelegator : IWinoRequestDelegator
|
||||
{
|
||||
private readonly IWinoRequestProcessor _winoRequestProcessor;
|
||||
private readonly IWinoSynchronizerFactory _winoSynchronizerFactory;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly ILogger _logger = Log.ForContext<WinoRequestDelegator>();
|
||||
|
||||
public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor,
|
||||
IWinoSynchronizerFactory winoSynchronizerFactory,
|
||||
IFolderService folderService,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_winoRequestProcessor = winoRequestProcessor;
|
||||
_winoSynchronizerFactory = winoSynchronizerFactory;
|
||||
_folderService = folderService;
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(MailOperationPreperationRequest request)
|
||||
{
|
||||
var requests = new List<IRequest>();
|
||||
|
||||
try
|
||||
{
|
||||
requests = await _winoRequestProcessor.PrepareRequestsAsync(request);
|
||||
}
|
||||
catch (UnavailableSpecialFolderException unavailableSpecialFolderException)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle,
|
||||
string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType),
|
||||
InfoBarMessageType.Warning,
|
||||
Translator.SettingConfigureSpecialFolders_Button,
|
||||
() =>
|
||||
{
|
||||
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
|
||||
});
|
||||
}
|
||||
catch (InvalidMoveTargetException)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Info_InvalidMoveTargetMessage, InfoBarMessageType.Warning);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
_dialogService.ShowNotSupportedMessage();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Request creation failed.");
|
||||
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
|
||||
if (requests == null || !requests.Any()) return;
|
||||
|
||||
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
|
||||
|
||||
// Queue requests for each account and start synchronization.
|
||||
foreach (var accountId in accountIds)
|
||||
{
|
||||
foreach (var accountRequest in accountId)
|
||||
{
|
||||
QueueRequest(accountRequest, accountId.Key);
|
||||
}
|
||||
|
||||
QueueSynchronization(accountId.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(FolderOperation operation, IMailItemFolder folderStructure)
|
||||
{
|
||||
IRequest request = null;
|
||||
|
||||
try
|
||||
{
|
||||
request = await _winoRequestProcessor.PrepareFolderRequestAsync(operation, folderStructure);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
_dialogService.ShowNotSupportedMessage();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Folder operation execution failed.");
|
||||
}
|
||||
|
||||
// _synchronizationWorker.Queue(request);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest)
|
||||
{
|
||||
var request = new CreateDraftRequest(draftPreperationRequest);
|
||||
|
||||
QueueRequest(request, draftPreperationRequest.Account.Id);
|
||||
QueueSynchronization(draftPreperationRequest.Account.Id);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest)
|
||||
{
|
||||
var request = new SendDraftRequest(sendDraftPreperationRequest);
|
||||
|
||||
QueueRequest(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
|
||||
QueueSynchronization(sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void QueueRequest(IRequest request, Guid accountId)
|
||||
{
|
||||
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(accountId);
|
||||
|
||||
if (synchronizer == null)
|
||||
{
|
||||
_logger.Warning("Synchronizer not found for account {AccountId}.", accountId);
|
||||
_logger.Warning("Skipping queueing request {Operation}.", request.Operation);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
synchronizer.QueueRequest(request);
|
||||
}
|
||||
|
||||
private void QueueSynchronization(Guid accountId)
|
||||
{
|
||||
var options = new SynchronizationOptions()
|
||||
{
|
||||
AccountId = accountId,
|
||||
Type = SynchronizationType.ExecuteRequests
|
||||
};
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
239
Wino.Core/Services/WinoRequestProcessor.cs
Normal file
239
Wino.Core/Services/WinoRequestProcessor.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Intermediary processor for converting a user action to executable Wino requests.
|
||||
/// Primarily responsible for batching requests by AccountId and FolderId.
|
||||
/// </summary>
|
||||
public class WinoRequestProcessor : BaseDatabaseService, IWinoRequestProcessor
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IKeyPressService _keyPressService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
/// <summary>
|
||||
/// Set of rules that defines which action should be executed if user wants to toggle an action.
|
||||
/// </summary>
|
||||
private readonly List<ToggleRequestRule> _toggleRequestRules =
|
||||
[
|
||||
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, bool>((item) => !item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<IMailItem, bool>((item) => item.IsFlagged)),
|
||||
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)),
|
||||
];
|
||||
|
||||
public WinoRequestProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IKeyPressService keyPressService,
|
||||
IPreferencesService preferencesService,
|
||||
IAccountService accountService,
|
||||
IDialogService dialogService) : base(databaseService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
_keyPressService = keyPressService;
|
||||
_preferencesService = preferencesService;
|
||||
_accountService = accountService;
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
public async Task<List<IRequest>> PrepareRequestsAsync(MailOperationPreperationRequest preperationRequest)
|
||||
{
|
||||
var action = preperationRequest.Action;
|
||||
var moveTargetStructure = preperationRequest.MoveTargetFolder;
|
||||
|
||||
// Ask confirmation for permanent delete operation.
|
||||
// Drafts are always hard deleted without any protection.
|
||||
|
||||
if (!preperationRequest.IgnoreHardDeleteProtection && ((action == MailOperation.SoftDelete && _keyPressService.IsShiftKeyPressed()) || action == MailOperation.HardDelete))
|
||||
{
|
||||
if (_preferencesService.IsHardDeleteProtectionEnabled)
|
||||
{
|
||||
var shouldDelete = await _dialogService.ShowHardDeleteConfirmationAsync();
|
||||
|
||||
if (!shouldDelete) return default;
|
||||
}
|
||||
|
||||
action = MailOperation.HardDelete;
|
||||
}
|
||||
|
||||
// Make sure there is a move target folder if action is move.
|
||||
// Let user pick a folder to move from the dialog.
|
||||
|
||||
if (action == MailOperation.Move && moveTargetStructure == null)
|
||||
{
|
||||
// TODO: Handle multiple accounts for move operation.
|
||||
// What happens if we move 2 different mails from 2 different accounts?
|
||||
|
||||
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
|
||||
|
||||
moveTargetStructure = await _dialogService.PickFolderAsync(accountId, PickFolderReason.Move, _folderService);
|
||||
|
||||
if (moveTargetStructure == null)
|
||||
return default;
|
||||
}
|
||||
|
||||
var requests = new List<IRequest>();
|
||||
|
||||
foreach (var item in preperationRequest.MailItems)
|
||||
{
|
||||
requests.Add(await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution));
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
private async Task<IRequest> GetSingleRequestAsync(MailCopy mailItem, MailOperation action, IMailItemFolder moveTargetStructure, bool shouldToggleActions)
|
||||
{
|
||||
if (mailItem.AssignedAccount == null) throw new ArgumentException(Translator.Exception_NullAssignedAccount);
|
||||
if (mailItem.AssignedFolder == null) throw new ArgumentException(Translator.Exception_NullAssignedFolder);
|
||||
|
||||
// Rule: Soft deletes from Trash folder must perform Hard Delete.
|
||||
if (action == MailOperation.SoftDelete && mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Deleted)
|
||||
action = MailOperation.HardDelete;
|
||||
|
||||
// Rule: SoftDelete draft items must be performed as hard delete.
|
||||
if (action == MailOperation.SoftDelete && mailItem.IsDraft)
|
||||
action = MailOperation.HardDelete;
|
||||
|
||||
// Rule: Toggle actions must be reverted if ToggleExecution is passed true.
|
||||
if (shouldToggleActions)
|
||||
{
|
||||
var toggleRule = _toggleRequestRules.Find(a => a.SourceAction == action);
|
||||
|
||||
if (toggleRule != null && toggleRule.Condition(mailItem))
|
||||
{
|
||||
action = toggleRule.TargetAction;
|
||||
}
|
||||
}
|
||||
|
||||
if (action == MailOperation.MarkAsRead)
|
||||
return new MarkReadRequest(mailItem, true);
|
||||
else if (action == MailOperation.MarkAsUnread)
|
||||
return new MarkReadRequest(mailItem, false);
|
||||
else if (action == MailOperation.SetFlag)
|
||||
return new ChangeFlagRequest(mailItem, true);
|
||||
else if (action == MailOperation.ClearFlag)
|
||||
return new ChangeFlagRequest(mailItem, false);
|
||||
else if (action == MailOperation.HardDelete)
|
||||
return new DeleteRequest(mailItem);
|
||||
else if (action == MailOperation.Move)
|
||||
{
|
||||
if (moveTargetStructure == null)
|
||||
throw new InvalidMoveTargetException();
|
||||
|
||||
// TODO
|
||||
// Rule: You can't move items to non-move target folders;
|
||||
// Rule: You can't move items from a folder to itself.
|
||||
|
||||
//if (!moveTargetStructure.IsMoveTarget || moveTargetStructure.FolderId == mailItem.AssignedFolder.Id)
|
||||
// throw new InvalidMoveTargetException();
|
||||
|
||||
var pickedFolderItem = await _folderService.GetFolderAsync(moveTargetStructure.Id);
|
||||
|
||||
return new MoveRequest(mailItem, mailItem.AssignedFolder, pickedFolderItem);
|
||||
}
|
||||
else if (action == MailOperation.Archive)
|
||||
{
|
||||
// Validate archive folder exists.
|
||||
|
||||
var archiveFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Archive)
|
||||
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Archive, mailItem.AssignedAccount.Id);
|
||||
|
||||
return new MoveRequest(mailItem, mailItem.AssignedFolder, archiveFolder);
|
||||
}
|
||||
else if (action == MailOperation.UnArchive || action == MailOperation.MarkAsNotJunk)
|
||||
{
|
||||
var inboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Inbox)
|
||||
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Inbox, mailItem.AssignedAccount.Id);
|
||||
|
||||
return new MoveRequest(mailItem, mailItem.AssignedFolder, inboxFolder);
|
||||
}
|
||||
else if (action == MailOperation.SoftDelete)
|
||||
{
|
||||
var trashFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Deleted)
|
||||
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Deleted, mailItem.AssignedAccount.Id);
|
||||
|
||||
return new MoveRequest(mailItem, mailItem.AssignedFolder, trashFolder);
|
||||
}
|
||||
else if (action == MailOperation.MoveToJunk)
|
||||
{
|
||||
var junkFolder = await _folderService.GetSpecialFolderByAccountIdAsync(mailItem.AssignedAccount.Id, SpecialFolderType.Junk)
|
||||
?? throw new UnavailableSpecialFolderException(SpecialFolderType.Junk, mailItem.AssignedAccount.Id);
|
||||
|
||||
return new MoveRequest(mailItem, mailItem.AssignedFolder, junkFolder);
|
||||
}
|
||||
else if (action == MailOperation.AlwaysMoveToFocused || action == MailOperation.AlwaysMoveToOther)
|
||||
return new AlwaysMoveToRequest(mailItem, action == MailOperation.AlwaysMoveToFocused);
|
||||
else
|
||||
throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedAction, action));
|
||||
}
|
||||
|
||||
public async Task<IRequest> PrepareFolderRequestAsync(FolderOperation operation, IMailItemFolder mailItemFolder)
|
||||
{
|
||||
if (mailItemFolder == null) return default;
|
||||
|
||||
var accountId = mailItemFolder.MailAccountId;
|
||||
|
||||
IRequest change = null;
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case FolderOperation.Pin:
|
||||
case FolderOperation.Unpin:
|
||||
await _folderService.ChangeStickyStatusAsync(mailItemFolder.Id, operation == FolderOperation.Pin);
|
||||
break;
|
||||
//case FolderOperation.MarkAllAsRead:
|
||||
// // Get all mails in the folder.
|
||||
|
||||
// var mailItems = await _folderService.GetAllUnreadItemsByFolderIdAsync(accountId, folderStructure.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
// if (mailItems.Any())
|
||||
// change = new FolderMarkAsReadRequest(accountId, mailItems.Select(a => a.Id).Distinct(), folderStructure.RemoteFolderId, folderStructure.FolderId);
|
||||
|
||||
// break;
|
||||
//case FolderOperation.Empty:
|
||||
// // Get all mails in the folder.
|
||||
|
||||
// var mailsToDelete = await _folderService.GetMailByFolderIdAsync(folderStructure.FolderId).ConfigureAwait(false);
|
||||
|
||||
// if (mailsToDelete.Any())
|
||||
// change = new FolderEmptyRequest(accountId, mailsToDelete.Select(a => a.Id).Distinct(), folderStructure.RemoteFolderId, folderStructure.FolderId);
|
||||
|
||||
// break;
|
||||
//case FolderOperation.Rename:
|
||||
// var newFolderName = await _dialogService.ShowRenameFolderDialogAsync(folderStructure.FolderName);
|
||||
|
||||
// if (!string.IsNullOrEmpty(newFolderName))
|
||||
// change = new RenameFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId, newFolderName, folderStructure.FolderName);
|
||||
|
||||
// break;
|
||||
//case FolderOperation.Delete:
|
||||
// var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete.");
|
||||
|
||||
// if (isConfirmed)
|
||||
// change = new DeleteFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId);
|
||||
|
||||
// break;
|
||||
//default:
|
||||
// throw new NotImplementedException();
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user