846 lines
32 KiB
C#
846 lines
32 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Diagnostics;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Serilog;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Entities.Calendar;
|
|
using Wino.Core.Domain.Entities.Mail;
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Accounts;
|
|
using Wino.Core.Domain.Misc;
|
|
using Wino.Messaging.Client.Calendar;
|
|
using Wino.Messaging.Client.Accounts;
|
|
using Wino.Messaging.UI;
|
|
|
|
namespace Wino.Services;
|
|
|
|
public class AccountService : BaseDatabaseService, IAccountService
|
|
{
|
|
public IAuthenticator ExternalAuthenticationAuthenticator { get; set; }
|
|
|
|
private readonly ISignatureService _signatureService;
|
|
private readonly IAuthenticationProvider _authenticationProvider;
|
|
private readonly IMimeFileService _mimeFileService;
|
|
private readonly IPreferencesService _preferencesService;
|
|
private readonly IContactPictureFileService _contactPictureFileService;
|
|
|
|
private readonly ILogger _logger = Log.ForContext<AccountService>();
|
|
|
|
public AccountService(IDatabaseService databaseService,
|
|
ISignatureService signatureService,
|
|
IAuthenticationProvider authenticationProvider,
|
|
IMimeFileService mimeFileService,
|
|
IPreferencesService preferencesService,
|
|
IContactPictureFileService contactPictureFileService) : base(databaseService)
|
|
{
|
|
_signatureService = signatureService;
|
|
_authenticationProvider = authenticationProvider;
|
|
_mimeFileService = mimeFileService;
|
|
_preferencesService = preferencesService;
|
|
_contactPictureFileService = contactPictureFileService;
|
|
}
|
|
|
|
|
|
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 accountIdList = linkedAccountIds.ToList();
|
|
var placeholders = string.Join(",", accountIdList.Select(_ => "?"));
|
|
var sql = $"UPDATE MailAccount SET MergedInboxId = ? WHERE Id IN ({placeholders})";
|
|
var parameters = new List<object> { mergedInboxId };
|
|
parameters.AddRange(accountIdList.Cast<object>());
|
|
|
|
await Connection.ExecuteAsync(sql, parameters.ToArray());
|
|
|
|
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
|
|
}
|
|
|
|
public async Task<string> UpdateSyncIdentifierRawAsync(Guid accountId, string syncIdentifier)
|
|
{
|
|
await Connection.ExecuteAsync("UPDATE MailAccount SET SynchronizationDeltaIdentifier = ? WHERE Id = ?", syncIdentifier, accountId);
|
|
return syncIdentifier;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
await Connection.ExecuteAsync("UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", mergedInboxId).ConfigureAwait(false);
|
|
await Connection.DeleteAsync<MergedInbox>(mergedInbox.Id).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, typeof(MailItemFolder));
|
|
}
|
|
|
|
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, typeof(MailItemFolder));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Insert merged inbox and assign accounts.
|
|
await Connection.InsertAsync(mergedInbox, typeof(MergedInbox));
|
|
|
|
foreach (var account in accountsToMerge)
|
|
{
|
|
account.MergedInboxId = mergedInbox.Id;
|
|
|
|
await Connection.UpdateAsync(account, typeof(MailAccount));
|
|
}
|
|
|
|
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
|
|
}
|
|
|
|
public async Task RenameMergedAccountAsync(Guid mergedInboxId, string newName)
|
|
{
|
|
await Connection.ExecuteAsync("UPDATE MergedInbox SET Name = ? WHERE Id = ?", newName, mergedInboxId);
|
|
|
|
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 with interactive authentication
|
|
// New authentication will include calendar scopes
|
|
var token = await authenticator.GenerateTokenInformationAsync(account);
|
|
|
|
Guard.IsNotNull(token);
|
|
|
|
// Enable calendar access since new token includes calendar scopes
|
|
account.IsCalendarAccessGranted = true;
|
|
|
|
await UpdateAccountAsync(account);
|
|
}
|
|
|
|
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>().OrderBy(a => a.Order).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;
|
|
}
|
|
|
|
public async Task<bool> AccountNameExistsAsync(string name, Guid? excludedAccountId = null)
|
|
{
|
|
var normalizedName = name?.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(normalizedName))
|
|
return false;
|
|
|
|
var accounts = await Connection.Table<MailAccount>().ToListAsync().ConfigureAwait(false);
|
|
|
|
return accounts.Any(account =>
|
|
account.Id != excludedAccountId &&
|
|
string.Equals(account.Name?.Trim(), normalizedName, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
public async Task<bool> AccountAddressExistsAsync(string address, Guid? excludedAccountId = null)
|
|
{
|
|
var normalizedAddress = address?.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(normalizedAddress))
|
|
return false;
|
|
|
|
var accounts = await Connection.Table<MailAccount>().ToListAsync().ConfigureAwait(false);
|
|
|
|
return accounts.Any(account =>
|
|
account.Id != excludedAccountId &&
|
|
string.Equals(account.Address?.Trim(), normalizedAddress, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
public async Task CreateRootAliasAsync(Guid accountId, string address)
|
|
{
|
|
var rootAlias = new MailAccountAlias()
|
|
{
|
|
AccountId = accountId,
|
|
AliasAddress = address,
|
|
IsPrimary = true,
|
|
IsRootAlias = true,
|
|
IsVerified = true,
|
|
ReplyToAddress = address,
|
|
Id = Guid.NewGuid(),
|
|
Source = AliasSource.Manual,
|
|
SendCapability = AliasSendCapability.Confirmed
|
|
};
|
|
|
|
await Connection.InsertAsync(rootAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
|
|
Log.Information("Created root alias for the account {AccountId}", accountId);
|
|
}
|
|
|
|
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
|
|
{
|
|
return await Connection.QueryAsync<MailAccountAlias>(
|
|
"SELECT * FROM MailAccountAlias WHERE AccountId = ? ORDER BY IsRootAlias DESC, IsPrimary DESC, AliasAddress ASC",
|
|
accountId).ConfigureAwait(false);
|
|
}
|
|
|
|
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
|
|
=> Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
|
|
|
|
public async Task DeleteAccountMailCacheAsync(Guid accountId, AccountCacheResetReason accountCacheResetReason)
|
|
{
|
|
await Connection.ExecuteAsync(
|
|
"DELETE FROM MailCopy WHERE Id IN (SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id FROM MailItemFolder WHERE MailAccountId = ?))",
|
|
accountId);
|
|
|
|
WeakReferenceMessenger.Default.Send(new AccountCacheResetMessage(accountId, accountCacheResetReason));
|
|
}
|
|
|
|
public async Task DeleteAccountAsync(MailAccount account)
|
|
{
|
|
// Collect calendar entities before deletion so we can notify UI subscribers.
|
|
var accountCalendars = await Connection.Table<AccountCalendar>()
|
|
.Where(a => a.AccountId == account.Id)
|
|
.ToListAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
var deletedCalendarItems = new List<CalendarItem>();
|
|
foreach (var accountCalendar in accountCalendars)
|
|
{
|
|
var calendarItems = await Connection.Table<CalendarItem>()
|
|
.Where(a => a.CalendarId == accountCalendar.Id)
|
|
.ToListAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
deletedCalendarItems.AddRange(calendarItems);
|
|
}
|
|
|
|
await DeleteAccountMailCacheAsync(account.Id, AccountCacheResetReason.AccountRemoval);
|
|
|
|
// Delete calendar metadata and related records for this account.
|
|
foreach (var calendarItem in deletedCalendarItems)
|
|
{
|
|
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false);
|
|
await Connection.Table<Reminder>().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false);
|
|
await Connection.Table<CalendarAttachment>().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false);
|
|
}
|
|
|
|
foreach (var accountCalendar in accountCalendars)
|
|
{
|
|
await Connection.Table<CalendarItem>().DeleteAsync(a => a.CalendarId == accountCalendar.Id).ConfigureAwait(false);
|
|
}
|
|
|
|
await Connection.Table<AccountCalendar>().DeleteAsync(a => a.AccountId == account.Id).ConfigureAwait(false);
|
|
|
|
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
|
|
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
|
|
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
|
|
|
|
// 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)
|
|
{
|
|
await Connection.ExecuteAsync(
|
|
"UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?",
|
|
account.MergedInboxId.Value).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
if (account.ProviderType == MailProviderType.IMAP4)
|
|
await Connection.Table<CustomServerInformation>().DeleteAsync(a => a.AccountId == account.Id);
|
|
|
|
if (account.Preferences != null)
|
|
await Connection.DeleteAsync<MailAccountPreferences>(account.Preferences.Id);
|
|
|
|
await Connection.DeleteAsync<MailAccount>(account.Id);
|
|
|
|
await _mimeFileService.DeleteUserMimeCacheAsync(account.Id).ConfigureAwait(false);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
foreach (var calendarItem in deletedCalendarItems)
|
|
{
|
|
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(calendarItem, EntityUpdateSource.Server));
|
|
}
|
|
|
|
foreach (var accountCalendar in accountCalendars)
|
|
{
|
|
WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar));
|
|
}
|
|
|
|
ReportUIChange(new AccountRemovedMessage(account));
|
|
}
|
|
|
|
public async Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation)
|
|
{
|
|
var account = await GetAccountAsync(accountId).ConfigureAwait(false);
|
|
|
|
if (account != null)
|
|
{
|
|
account.SenderName = profileInformation.SenderName;
|
|
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
|
|
|
if (string.IsNullOrEmpty(account.Address))
|
|
{
|
|
account.Address = profileInformation.AccountAddress;
|
|
}
|
|
// Forcefully add or update a contact data with the provided information.
|
|
|
|
var existingContact = await Connection.Table<AccountContact>()
|
|
.FirstOrDefaultAsync(a => a.Address == account.Address)
|
|
.ConfigureAwait(false);
|
|
|
|
var contactPictureFileId = await SaveProfilePictureAsync(
|
|
account.Base64ProfilePictureData,
|
|
existingContact?.ContactPictureFileId).ConfigureAwait(false);
|
|
|
|
var accountContact = new AccountContact()
|
|
{
|
|
Address = account.Address,
|
|
Name = account.SenderName,
|
|
ContactPictureFileId = contactPictureFileId,
|
|
IsRootContact = true
|
|
};
|
|
|
|
await Connection.InsertOrReplaceAsync(accountContact, typeof(AccountContact)).ConfigureAwait(false);
|
|
|
|
await UpdateAccountAsync(account).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task<Guid?> SaveProfilePictureAsync(string base64ProfilePictureData, Guid? existingFileId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(base64ProfilePictureData))
|
|
{
|
|
if (existingFileId.HasValue)
|
|
await _contactPictureFileService.DeleteContactPictureAsync(existingFileId.Value).ConfigureAwait(false);
|
|
|
|
return null;
|
|
}
|
|
|
|
byte[] bytes;
|
|
try
|
|
{
|
|
bytes = Convert.FromBase64String(base64ProfilePictureData);
|
|
}
|
|
catch (FormatException ex)
|
|
{
|
|
_logger.Warning(ex, "Failed to decode account profile picture for contact migration.");
|
|
return existingFileId;
|
|
}
|
|
|
|
var newFileId = await _contactPictureFileService.SaveContactPictureAsync(bytes).ConfigureAwait(false);
|
|
|
|
if (existingFileId.HasValue)
|
|
await _contactPictureFileService.DeleteContactPictureAsync(existingFileId.Value).ConfigureAwait(false);
|
|
|
|
return newFileId;
|
|
}
|
|
|
|
public async Task<MailAccount> GetAccountAsync(Guid accountId)
|
|
{
|
|
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
|
|
|
if (account == null)
|
|
{
|
|
_logger.Error("Could not find account with id {AccountId}", accountId);
|
|
}
|
|
else
|
|
{
|
|
if (account.ProviderType == MailProviderType.IMAP4)
|
|
account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id);
|
|
|
|
account.Preferences = await GetAccountPreferencesAsync(account.Id);
|
|
|
|
return account;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Task<CustomServerInformation> GetAccountCustomServerInformationAsync(Guid accountId)
|
|
=> Connection.Table<CustomServerInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
|
|
|
public async Task UpdateAccountAsync(MailAccount account)
|
|
{
|
|
await Connection.UpdateAsync(account.Preferences, typeof(MailAccountPreferences)).ConfigureAwait(false);
|
|
await Connection.UpdateAsync(account, typeof(MailAccount)).ConfigureAwait(false);
|
|
|
|
ReportUIChange(new AccountUpdatedMessage(account));
|
|
}
|
|
|
|
public async Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation)
|
|
{
|
|
await Connection.InsertOrReplaceAsync(customServerInformation, typeof(CustomServerInformation)).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
|
|
{
|
|
// Delete existing ones.
|
|
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
|
|
|
|
// Insert new ones.
|
|
foreach (var alias in aliases)
|
|
{
|
|
await Connection.InsertAsync(alias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
|
{
|
|
var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
|
|
var normalizedRemoteAliases = remoteAccountAliases ?? [];
|
|
|
|
foreach (var remoteAlias in normalizedRemoteAliases)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(remoteAlias?.AliasAddress))
|
|
continue;
|
|
|
|
var existingAlias = localAliases.Find(a =>
|
|
a.AccountId == account.Id &&
|
|
a.AliasAddress.Equals(remoteAlias.AliasAddress, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (existingAlias == null)
|
|
{
|
|
// Create new alias.
|
|
var newAlias = new MailAccountAlias()
|
|
{
|
|
AccountId = account.Id,
|
|
AliasAddress = remoteAlias.AliasAddress,
|
|
IsPrimary = remoteAlias.IsPrimary,
|
|
IsVerified = remoteAlias.IsVerified,
|
|
ReplyToAddress = remoteAlias.ReplyToAddress,
|
|
Id = Guid.NewGuid(),
|
|
IsRootAlias = remoteAlias.IsRootAlias,
|
|
AliasSenderName = remoteAlias.AliasSenderName,
|
|
Source = remoteAlias.Source,
|
|
SendCapability = remoteAlias.SendCapability
|
|
};
|
|
|
|
await Connection.InsertAsync(newAlias, typeof(MailAccountAlias));
|
|
localAliases.Add(newAlias);
|
|
}
|
|
else
|
|
{
|
|
// Update existing alias.
|
|
existingAlias.IsPrimary = remoteAlias.IsPrimary;
|
|
existingAlias.IsVerified = remoteAlias.IsVerified;
|
|
existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
|
|
existingAlias.AliasSenderName = remoteAlias.AliasSenderName;
|
|
existingAlias.Source = remoteAlias.Source;
|
|
existingAlias.SendCapability = remoteAlias.SendCapability;
|
|
|
|
await Connection.UpdateAsync(existingAlias, typeof(MailAccountAlias));
|
|
}
|
|
}
|
|
|
|
if (localAliases.Count == 0 && !string.IsNullOrWhiteSpace(account.Address))
|
|
{
|
|
var fallbackAddress = account.Address.Trim();
|
|
var fallbackAlias = new MailAccountAlias()
|
|
{
|
|
AccountId = account.Id,
|
|
AliasAddress = fallbackAddress,
|
|
IsPrimary = true,
|
|
IsRootAlias = true,
|
|
IsVerified = true,
|
|
ReplyToAddress = fallbackAddress,
|
|
Id = Guid.NewGuid(),
|
|
Source = AliasSource.ProviderDiscovered,
|
|
SendCapability = AliasSendCapability.Confirmed
|
|
};
|
|
|
|
await Connection.InsertAsync(fallbackAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
localAliases.Add(fallbackAlias);
|
|
}
|
|
|
|
// Make sure there is only 1 root alias and 1 primary alias selected.
|
|
|
|
bool shouldUpdatePrimary = localAliases.Count(a => a.IsPrimary) != 1;
|
|
bool shouldUpdateRoot = localAliases.Count(a => a.IsRootAlias) != 1;
|
|
|
|
if (shouldUpdatePrimary)
|
|
{
|
|
localAliases.ForEach(a => a.IsPrimary = false);
|
|
|
|
var idealPrimaryAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
|
|
|
|
idealPrimaryAlias.IsPrimary = true;
|
|
await Connection.UpdateAsync(idealPrimaryAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
}
|
|
|
|
if (shouldUpdateRoot)
|
|
{
|
|
localAliases.ForEach(a => a.IsRootAlias = false);
|
|
|
|
var idealRootAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
|
|
|
|
idealRootAlias.IsRootAlias = true;
|
|
await Connection.UpdateAsync(idealRootAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
public async Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(aliasAddress))
|
|
return;
|
|
|
|
var aliases = await GetAccountAliasesAsync(accountId).ConfigureAwait(false);
|
|
var alias = aliases.FirstOrDefault(a => a.AliasAddress.Equals(aliasAddress, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (alias == null)
|
|
return;
|
|
|
|
alias.SendCapability = capability;
|
|
await Connection.UpdateAsync(alias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task DeleteAccountAliasAsync(Guid aliasId)
|
|
{
|
|
// Create query to delete alias.
|
|
|
|
await Connection.ExecuteAsync("DELETE FROM MailAccountAlias WHERE Id = ?", aliasId).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation)
|
|
{
|
|
Guard.IsNotNull(account);
|
|
|
|
if (await AccountNameExistsAsync(account.Name).ConfigureAwait(false))
|
|
throw new InvalidOperationException(Translator.DialogMessage_AccountNameExistsMessage);
|
|
|
|
if (await AccountAddressExistsAsync(account.Address).ConfigureAwait(false))
|
|
throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage);
|
|
|
|
if (!account.CreatedAt.HasValue)
|
|
{
|
|
account.CreatedAt = DateTime.UtcNow;
|
|
}
|
|
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
// Set the order of the account.
|
|
// This can be changed by the user later in manage accounts page.
|
|
account.Order = accountCount;
|
|
}
|
|
|
|
await Connection.InsertAsync(account, typeof(MailAccount));
|
|
|
|
var preferences = new MailAccountPreferences()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
AccountId = account.Id,
|
|
IsNotificationsEnabled = true,
|
|
ShouldAppendMessagesToSentFolder = false
|
|
};
|
|
|
|
// iCloud does not appends sent messages to sent folder automatically.
|
|
if (account.SpecialImapProvider == SpecialImapProvider.iCloud || account.SpecialImapProvider == SpecialImapProvider.Yahoo)
|
|
{
|
|
preferences.ShouldAppendMessagesToSentFolder = true;
|
|
}
|
|
|
|
account.Preferences = preferences;
|
|
|
|
// Outlook & Office 365 supports Focused inbox. Enabled by default.
|
|
bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook;
|
|
|
|
// TODO: This should come from account settings API.
|
|
// Wino doesn't have MailboxSettings yet.
|
|
if (isMicrosoftProvider)
|
|
account.Preferences.IsFocusedInboxEnabled = true;
|
|
|
|
// Setup default signature.
|
|
var defaultSignature = await _signatureService.CreateDefaultSignatureAsync(account.Id);
|
|
|
|
account.Preferences.SignatureIdForNewMessages = defaultSignature.Id;
|
|
account.Preferences.SignatureIdForFollowingMessages = defaultSignature.Id;
|
|
account.Preferences.IsSignatureEnabled = true;
|
|
|
|
await Connection.InsertAsync(preferences, typeof(MailAccountPreferences));
|
|
|
|
if (customServerInformation != null)
|
|
await Connection.InsertAsync(customServerInformation, typeof(CustomServerInformation));
|
|
|
|
if (account.ProviderType == MailProviderType.IMAP4 &&
|
|
customServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.LocalOnly)
|
|
{
|
|
await EnsureDefaultLocalCalendarForImapAsync(account.Id).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task EnsureDefaultLocalCalendarForImapAsync(Guid accountId)
|
|
{
|
|
var existingCalendarCount = await Connection.Table<AccountCalendar>()
|
|
.Where(a => a.AccountId == accountId)
|
|
.CountAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
if (existingCalendarCount > 0)
|
|
return;
|
|
|
|
var localCalendar = new AccountCalendar
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
AccountId = accountId,
|
|
Name = Translator.AccountDetailsPage_TabCalendar,
|
|
IsPrimary = true,
|
|
IsSynchronizationEnabled = true,
|
|
IsExtended = true,
|
|
RemoteCalendarId = string.Empty,
|
|
TimeZone = string.Empty,
|
|
BackgroundColorHex = await GetNextDistinctCalendarColorAsync().ConfigureAwait(false)
|
|
};
|
|
|
|
localCalendar.TextColorHex = GetReadableTextColorHex(localCalendar.BackgroundColorHex);
|
|
|
|
await Connection.InsertAsync(localCalendar, typeof(AccountCalendar)).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task<string> GetNextDistinctCalendarColorAsync()
|
|
{
|
|
var usedColors = await Connection.Table<AccountCalendar>()
|
|
.ToListAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
return CalendarColorPalette.GetDistinctColor(usedColors.Select(a => a.BackgroundColorHex));
|
|
}
|
|
|
|
private static string GetReadableTextColorHex(string backgroundColorHex)
|
|
{
|
|
if (!TryParseHexColor(backgroundColorHex, out var red, out var green, out var blue))
|
|
return "#FFFFFF";
|
|
|
|
var luminance = ((0.299 * red) + (0.587 * green) + (0.114 * blue)) / 255d;
|
|
return luminance > 0.6 ? "#111111" : "#FFFFFF";
|
|
}
|
|
|
|
private static bool TryParseHexColor(string value, out int red, out int green, out int blue)
|
|
{
|
|
red = 255;
|
|
green = 255;
|
|
blue = 255;
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return false;
|
|
|
|
var color = value.Trim();
|
|
if (color.StartsWith('#'))
|
|
{
|
|
color = color[1..];
|
|
}
|
|
|
|
if (color.Length != 6 ||
|
|
!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
red = Convert.ToInt32(color.Substring(0, 2), 16);
|
|
green = Convert.ToInt32(color.Substring(2, 2), 16);
|
|
blue = Convert.ToInt32(color.Substring(4, 2), 16);
|
|
return true;
|
|
}
|
|
|
|
public async Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair)
|
|
{
|
|
foreach (var pair in accountIdOrderPair)
|
|
{
|
|
var account = await GetAccountAsync(pair.Key);
|
|
|
|
if (account == null)
|
|
{
|
|
_logger.Information("Could not find account with id {Key} for reordering. It may be a linked account.", pair.Key);
|
|
continue;
|
|
}
|
|
|
|
account.Order = pair.Value;
|
|
|
|
await Connection.UpdateAsync(account, typeof(MailAccount));
|
|
}
|
|
|
|
Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair));
|
|
}
|
|
|
|
public async Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId)
|
|
{
|
|
var aliases = await GetAccountAliasesAsync(accountId);
|
|
|
|
if (aliases == null || aliases.Count == 0) return null;
|
|
|
|
return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
|
|
}
|
|
|
|
public async Task<bool> IsAccountFocusedEnabledAsync(Guid accountId)
|
|
{
|
|
var account = await GetAccountAsync(accountId);
|
|
return account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
|
|
}
|
|
|
|
public async Task<bool> IsNotificationsEnabled(Guid accountId)
|
|
{
|
|
var account = await GetAccountAsync(accountId);
|
|
|
|
return account?.Preferences?.IsNotificationsEnabled ?? false;
|
|
}
|
|
|
|
public async Task UpdateLastFolderStructureSyncDateAsync(Guid accountId)
|
|
{
|
|
var account = await GetAccountAsync(accountId);
|
|
if (account == null) return;
|
|
|
|
account.LastFolderStructureSyncDate = DateTime.UtcNow;
|
|
await Connection.UpdateAsync(account, typeof(MailAccount)).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<bool> ShouldSyncFolderStructureAsync(Guid accountId, TimeSpan syncInterval)
|
|
{
|
|
var account = await GetAccountAsync(accountId);
|
|
if (account == null) return true;
|
|
|
|
if (!account.LastFolderStructureSyncDate.HasValue)
|
|
return true;
|
|
|
|
return DateTime.UtcNow - account.LastFolderStructureSyncDate.Value > syncInterval;
|
|
}
|
|
}
|