diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs
index 60568d32..c1bc43a1 100644
--- a/Wino.Core.Domain/Entities/MailAccount.cs
+++ b/Wino.Core.Domain/Entities/MailAccount.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Enums;
@@ -48,7 +47,7 @@ namespace Wino.Core.Domain.Entities
///
/// Base64 encoded profile picture of the account.
///
- public string ProfilePictureBase64 { get; set; }
+ public string Base64ProfilePictureData { get; set; }
///
/// Gets or sets the listing order of the account in the accounts list.
@@ -84,8 +83,8 @@ namespace Wino.Core.Domain.Entities
/// It's only synchronized for Gmail right now.
/// Other provider types are manually added by users and not verified.
///
- [Ignore]
- public List Aliases { get; set; }
+ //[Ignore]
+ //public List Aliases { get; set; }
///
/// Account preferences.
diff --git a/Wino.Core.Domain/Entities/MailAccountAlias.cs b/Wino.Core.Domain/Entities/MailAccountAlias.cs
index f1f0b623..8d103744 100644
--- a/Wino.Core.Domain/Entities/MailAccountAlias.cs
+++ b/Wino.Core.Domain/Entities/MailAccountAlias.cs
@@ -32,6 +32,13 @@ namespace Wino.Core.Domain.Entities
///
public bool IsPrimary { get; set; }
+ ///
+ /// Whether this alias is the root alias for the account.
+ /// Root alias means the first alias that was created for the account.
+ /// It can't be deleted or changed.
+ ///
+ public bool IsRootAlias { get; set; }
+
///
/// Whether the alias is verified by the server.
/// Non-verified aliases will show an info tip to users during sending.
@@ -40,6 +47,11 @@ namespace Wino.Core.Domain.Entities
///
public bool IsVerified { get; set; }
+ ///
+ /// Root aliases can't be deleted.
+ ///
+ public bool CanDelete => !IsRootAlias;
+
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
@@ -51,17 +63,20 @@ namespace Wino.Core.Domain.Entities
AliasAddress == other.AliasAddress &&
ReplyToAddress == other.ReplyToAddress &&
IsPrimary == other.IsPrimary &&
- IsVerified == other.IsVerified;
+ IsVerified == other.IsVerified &&
+ IsRootAlias == other.IsRootAlias;
}
public override int GetHashCode()
{
- int hashCode = -753829106;
+ int hashCode = 59052167;
hashCode = hashCode * -1521134295 + AccountId.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AliasAddress);
hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ReplyToAddress);
hashCode = hashCode * -1521134295 + IsPrimary.GetHashCode();
+ hashCode = hashCode * -1521134295 + IsRootAlias.GetHashCode();
hashCode = hashCode * -1521134295 + IsVerified.GetHashCode();
+ hashCode = hashCode * -1521134295 + CanDelete.GetHashCode();
return hashCode;
}
}
diff --git a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
index af724f47..947449d4 100644
--- a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
+++ b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
@@ -9,6 +9,7 @@
ManuelSetupWaiting,
TestingConnection,
AutoDiscoverySetup,
- AutoDiscoveryInProgress
+ AutoDiscoveryInProgress,
+ FetchingProfileInformation
}
}
diff --git a/Wino.Core.Domain/Enums/SynchronizationType.cs b/Wino.Core.Domain/Enums/SynchronizationType.cs
index c95b7d52..927016d6 100644
--- a/Wino.Core.Domain/Enums/SynchronizationType.cs
+++ b/Wino.Core.Domain/Enums/SynchronizationType.cs
@@ -7,5 +7,6 @@
Inbox, // Only Inbox
Custom, // Only sync folders that are specified in the options.
Full, // Synchronize everything
+ UpdateProfile, // Only update profile information
}
}
diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs
index 2a7264a2..cf3b5292 100644
--- a/Wino.Core.Domain/Enums/WinoPage.cs
+++ b/Wino.Core.Domain/Enums/WinoPage.cs
@@ -23,5 +23,6 @@
LanguageTimePage,
AppPreferencesPage,
SettingOptionsPage,
+ AliasManagementPage
}
}
diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs
index 926a65d6..e7196653 100644
--- a/Wino.Core.Domain/Interfaces/IAccountService.cs
+++ b/Wino.Core.Domain/Interfaces/IAccountService.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities;
+using Wino.Core.Domain.Models.Accounts;
namespace Wino.Core.Domain.Interfaces
{
@@ -101,12 +102,42 @@ namespace Wino.Core.Domain.Interfaces
/// AccountId-OrderNumber pair for all accounts.
Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair);
+ ///
+ /// Returns the account aliases.
+ ///
+ /// Account id.
+ /// A list of MailAccountAlias that has e-mail aliases.
+ Task> GetAccountAliasesAsync(Guid accountId);
+
///
/// Updated account's aliases.
///
/// Account id to update aliases for.
/// Full list of updated aliases.
///
- Task UpdateAccountAliases(Guid accountId, List aliases);
+ Task UpdateAccountAliasesAsync(Guid accountId, List aliases);
+
+ ///
+ /// Delete account alias.
+ ///
+ /// Alias to remove.
+ Task DeleteAccountAliasAsync(Guid aliasId);
+
+ ///
+ /// Updated profile information of the account.
+ ///
+ /// Account id to update info for.
+ /// Info data.
+ ///
+ Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation);
+
+
+ ///
+ /// Creates a root + primary alias for the account.
+ /// This is only called when the account is created.
+ ///
+ /// Account id.
+ /// Address to create root primary alias from.
+ Task CreateRootAliasAsync(Guid accountId, string address);
}
}
diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
index 21ca1f8a..e6ccd2a1 100644
--- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
+++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MailKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
@@ -43,6 +44,13 @@ namespace Wino.Core.Domain.Interfaces
/// Result summary of synchronization.
Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+ ///
+ /// Synchronizes profile information with the server.
+ /// Sender name and
+ ///
+ ///
+ Task SynchronizeProfileInformationAsync();
+
///
/// Downloads a single MIME message from the server and saves it to disk.
///
diff --git a/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
new file mode 100644
index 00000000..d27a0229
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
@@ -0,0 +1,9 @@
+using Wino.Core.Domain.Entities;
+
+namespace Wino.Core.Domain.Interfaces
+{
+ public interface ICreateAccountAliasDialog
+ {
+ public MailAccountAlias CreatedAccountAlias { get; set; }
+ }
+}
diff --git a/Wino.Core.Domain/Interfaces/IDialogService.cs b/Wino.Core.Domain/Interfaces/IDialogService.cs
index a2986bd9..9d5fad1b 100644
--- a/Wino.Core.Domain/Interfaces/IDialogService.cs
+++ b/Wino.Core.Domain/Interfaces/IDialogService.cs
@@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces
///
/// Signature information. Null if canceled.
Task ShowSignatureEditorDialog(AccountSignature signatureModel = null);
+ Task ShowCreateAccountAliasDialogAsync();
}
}
diff --git a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
index a1081b9e..428d1307 100644
--- a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
+++ b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
@@ -2,5 +2,5 @@
namespace Wino.Core.Domain.Models.Accounts
{
- public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string SenderName, string AccountColorHex = "");
+ public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string AccountColorHex = "");
}
diff --git a/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
new file mode 100644
index 00000000..b9fb5677
--- /dev/null
+++ b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
@@ -0,0 +1,9 @@
+namespace Wino.Core.Domain.Models.Accounts
+{
+ ///
+ /// Encapsulates the profile information of an account.
+ ///
+ /// Display sender name for the account.
+ /// Base 64 encoded profile picture data of the account. Thumbnail size.
+ public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
+}
diff --git a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
index 4f794749..dc9a5824 100644
--- a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
+++ b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
@@ -14,7 +14,6 @@ namespace Wino.Core.Domain.Models.Accounts
public string ProviderImage => $"ms-appx:///Assets/Providers/{Type}.png";
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
- public bool RequireSenderNameOnCreationDialog => Type != MailProviderType.IMAP4;
public ProviderDetail(MailProviderType type)
{
diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
index 19e45eb7..885a4b9b 100644
--- a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
+++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Synchronization
@@ -15,14 +16,18 @@ namespace Wino.Core.Domain.Models.Synchronization
/// It's ignored in serialization. Client should not react to this.
///
[JsonIgnore]
- public IEnumerable DownloadedMessages { get; set; } = new List();
+ public IEnumerable DownloadedMessages { get; set; } = [];
+
+ public ProfileInformation ProfileInformation { get; set; }
+
public SynchronizationCompletedState CompletedState { get; set; }
public static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
- public static SynchronizationResult Completed(IEnumerable downloadedMessages)
- => new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success };
+ public static SynchronizationResult Completed(IEnumerable downloadedMessages, ProfileInformation profileInformation = null)
+ => new() { DownloadedMessages = downloadedMessages, ProfileInformation = profileInformation, CompletedState = SynchronizationCompletedState.Success };
public static SynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled };
+ public static SynchronizationResult Failed => new() { CompletedState = SynchronizationCompletedState.Failed };
}
}
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index 2f6699c7..b77392dc 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -3,6 +3,7 @@
"AccountCreationDialog_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.",
+ "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.",
"AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account",
"AccountPickerDialog_Title": "Pick an account",
@@ -64,6 +65,14 @@
"CustomThemeBuilder_WallpaperTitle": "Set custom wallpaper",
"DialogMessage_AccountLimitMessage": "You have reached the account creation limit.\nWould you like to purchase 'Unlimited Account' add-on to continue?",
"DialogMessage_AccountLimitTitle": "Account Limit Reached",
+ "DialogMessage_AliasExistsTitle": "Existing Alias",
+ "DialogMessage_AliasExistsMessage": "This alias is already in use.",
+ "DialogMessage_InvalidAliasTitle": "Invalid Alias",
+ "DialogMessage_InvalidAliasMessage": "This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.",
+ "DialogMessage_CantDeleteRootAliasTitle": "Can't Delete Alias",
+ "DialogMessage_CantDeleteRootAliasMessage": "Root alias can't be deleted. This is your main identity associated with your account setup.",
+ "DialogMessage_AliasCreatedTitle": "Created New Alias",
+ "DialogMessage_AliasCreatedMessage": "New alias is succesfully created.",
"DialogMessage_CleanupFolderMessage": "Do you want to permanently delete all the mails in this folder?",
"DialogMessage_CleanupFolderTitle": "Cleanup Folder",
"DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.",
@@ -92,6 +101,12 @@
"DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website",
"DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.",
"Dialog_DontAskAgain": "Don't ask again",
+ "CreateAccountAliasDialog_Title": "Create Account Alias",
+ "CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
+ "CreateAccountAliasDialog_AliasAddress": "Address",
+ "CreateAccountAliasDialog_AliasAddressPlaceholder": "eg. support@mydomain.com",
+ "CreateAccountAliasDialog_ReplyToAddress": "Reply-To Address",
+ "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mydomain.com",
"DiscordChannelDisclaimerMessage": "Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server.\nTo get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects'\n\nYou will be directed to server URL since Discord doesn't support channel invites.",
"DiscordChannelDisclaimerTitle": "Important Discord Information",
"Draft": "Draft",
@@ -113,6 +128,7 @@
"Exception_CustomThemeMissingName": "You must provide a name.",
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
"Exception_FailedToSynchronizeFolders": "Failed to synchronize folders",
+ "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information",
"Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.",
"Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.",
"Exception_GoogleAuthError": "OAuth authorization error: {0}",
@@ -253,6 +269,7 @@
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
"Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.",
"Info_UnsubscribeErrorMessage": "Failed to unsubscribe",
+ "Info_CantDeletePrimaryAliasMessage": "Primary alias can't be deleted. Please change your alias before deleting this one",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
"ImapAuthenticationMethod_Auto": "Auto",
@@ -396,6 +413,8 @@
"SettingsFolderSync_Title": "Folder Synchronization",
"SettingsFolderOptions_Title": "Folder Configuration",
"SettingsFolderOptions_Description": "Change individual folder settings like enable/disable sync or show/hide unread badge.",
+ "SettingsManageAliases_Title": "Aliases",
+ "SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.",
"SettingsHoverActionCenter": "Center Action",
"SettingsHoverActionLeft": "Left Action",
"SettingsHoverActionRight": "Right Action",
@@ -406,6 +425,11 @@
"SettingsLanguageTime_Title": "Language & Time",
"SettingsLanguageTime_Description": "Wino display language, preferred time format.",
"CategoriesFolderNameOverride": "Categories",
+ "AccountAlias_Column_Verified": "Verified",
+ "AccountAlias_Column_Alias": "Alias",
+ "AccountAlias_Column_IsPrimaryAlias": "Primary",
+ "AccountAlias_Disclaimer_FirstLine": "Wino can only import aliases for your Gmail accounts.",
+ "AccountAlias_Disclaimer_SecondLine": "If you want to use aliases for your Outlook or IMAP account, please add them yourself.",
"MoreFolderNameOverride": "More",
"SettingsOptions_Title": "Settings",
"SettingsLinkAccounts_Description": "Merge multiple accounts into one. See mails from one Inbox together.",
diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs
index 6fc65a03..a20c9711 100644
--- a/Wino.Core.Domain/Translator.Designer.cs
+++ b/Wino.Core.Domain/Translator.Designer.cs
@@ -38,6 +38,11 @@ namespace Wino.Core.Domain
///
public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn");
+ ///
+ /// Fetching profile details.
+ ///
+ public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation");
+
///
/// Account Name
///
@@ -343,6 +348,46 @@ namespace Wino.Core.Domain
///
public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle");
+ ///
+ /// Existing Alias
+ ///
+ public static string DialogMessage_AliasExistsTitle => Resources.GetTranslatedString(@"DialogMessage_AliasExistsTitle");
+
+ ///
+ /// This alias is already in use.
+ ///
+ public static string DialogMessage_AliasExistsMessage => Resources.GetTranslatedString(@"DialogMessage_AliasExistsMessage");
+
+ ///
+ /// Invalid Alias
+ ///
+ public static string DialogMessage_InvalidAliasTitle => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasTitle");
+
+ ///
+ /// This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.
+ ///
+ public static string DialogMessage_InvalidAliasMessage => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasMessage");
+
+ ///
+ /// Can't Delete Alias
+ ///
+ public static string DialogMessage_CantDeleteRootAliasTitle => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasTitle");
+
+ ///
+ /// Root alias can't be deleted. This is your main identity associated with your account setup.
+ ///
+ public static string DialogMessage_CantDeleteRootAliasMessage => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasMessage");
+
+ ///
+ /// Created New Alias
+ ///
+ public static string DialogMessage_AliasCreatedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedTitle");
+
+ ///
+ /// New alias is succesfully created.
+ ///
+ public static string DialogMessage_AliasCreatedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedMessage");
+
///
/// Do you want to permanently delete all the mails in this folder?
///
@@ -434,7 +479,7 @@ namespace Wino.Core.Domain
public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle");
///
- /// Missin Subject
+ /// Missing Subject
///
public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation");
@@ -483,6 +528,36 @@ namespace Wino.Core.Domain
///
public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain");
+ ///
+ /// Create Account Alias
+ ///
+ public static string CreateAccountAliasDialog_Title => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Title");
+
+ ///
+ /// Make sure your outgoing server allows sending mails from this alias.
+ ///
+ public static string CreateAccountAliasDialog_Description => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Description");
+
+ ///
+ /// Address
+ ///
+ public static string CreateAccountAliasDialog_AliasAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddress");
+
+ ///
+ /// eg. support@mydomain.com
+ ///
+ public static string CreateAccountAliasDialog_AliasAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddressPlaceholder");
+
+ ///
+ /// Reply-To Address
+ ///
+ public static string CreateAccountAliasDialog_ReplyToAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddress");
+
+ ///
+ /// admin@mydomain.com
+ ///
+ public static string CreateAccountAliasDialog_ReplyToAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddressPlaceholder");
+
///
/// Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server. To get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects' You will be directed to server URL since Discord doesn't support channel invites.
///
@@ -588,6 +663,11 @@ namespace Wino.Core.Domain
///
public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders");
+ ///
+ /// Failed to synchronize profile information
+ ///
+ public static string Exception_FailedToSynchronizeProfileInformation => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeProfileInformation");
+
///
/// Callback uri is null on activation.
///
@@ -1288,6 +1368,11 @@ namespace Wino.Core.Domain
///
public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage");
+ ///
+ /// Primary alias can't be deleted. Please change your alias before deleting this one
+ ///
+ public static string Info_CantDeletePrimaryAliasMessage => Resources.GetTranslatedString(@"Info_CantDeletePrimaryAliasMessage");
+
///
/// Authentication method
///
@@ -2003,6 +2088,16 @@ namespace Wino.Core.Domain
///
public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description");
+ ///
+ /// Aliases
+ ///
+ public static string SettingsManageAliases_Title => Resources.GetTranslatedString(@"SettingsManageAliases_Title");
+
+ ///
+ /// See e-mail aliases assigned for this account, update or delete them.
+ ///
+ public static string SettingsManageAliases_Description => Resources.GetTranslatedString(@"SettingsManageAliases_Description");
+
///
/// Center Action
///
@@ -2053,6 +2148,31 @@ namespace Wino.Core.Domain
///
public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride");
+ ///
+ /// Verified
+ ///
+ public static string AccountAlias_Column_Verified => Resources.GetTranslatedString(@"AccountAlias_Column_Verified");
+
+ ///
+ /// Alias
+ ///
+ public static string AccountAlias_Column_Alias => Resources.GetTranslatedString(@"AccountAlias_Column_Alias");
+
+ ///
+ /// Primary
+ ///
+ public static string AccountAlias_Column_IsPrimaryAlias => Resources.GetTranslatedString(@"AccountAlias_Column_IsPrimaryAlias");
+
+ ///
+ /// Wino can only import aliases for your Gmail accounts.
+ ///
+ public static string AccountAlias_Disclaimer_FirstLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_FirstLine");
+
+ ///
+ /// If you want to use aliases for your Outlook or IMAP account, please add them yourself.
+ ///
+ public static string AccountAlias_Disclaimer_SecondLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_SecondLine");
+
///
/// More
///
diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
index 263669ab..ed290d13 100644
--- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
@@ -205,21 +205,22 @@ namespace Wino.Core.Extensions
};
}
- public static List GetMailAliases(this ListSendAsResponse response, MailAccount currentAccount)
+ public static List GetMailAliases(this ListSendAsResponse response, List currentAliases, MailAccount account)
{
- if (response == null || response.SendAs == null) return currentAccount.Aliases;
+ if (response == null || response.SendAs == null) return currentAliases;
var remoteAliases = response.SendAs.Select(a => new MailAccountAlias()
{
- AccountId = currentAccount.Id,
+ AccountId = account.Id,
AliasAddress = a.SendAsEmail,
IsPrimary = a.IsPrimary.GetValueOrDefault(),
- ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? currentAccount.Address : a.ReplyToAddress,
+ ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? account.Address : a.ReplyToAddress,
IsVerified = string.IsNullOrEmpty(a.VerificationStatus) ? true : a.VerificationStatus == "accepted",
+ IsRootAlias = account.Address == a.SendAsEmail,
Id = Guid.NewGuid()
}).ToList();
- return EntityExtensions.GetFinalAliasList(currentAccount.Aliases, remoteAliases);
+ return EntityExtensions.GetFinalAliasList(currentAliases, remoteAliases);
}
}
}
diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
index 6ab9ef2d..46116d72 100644
--- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
@@ -40,6 +40,8 @@ namespace Wino.Core.Integration.Processors
/// All folders.
Task> GetLocalFoldersAsync(Guid accountId);
+ Task> GetAccountAliasesAsync(Guid accountId);
+
Task> GetSynchronizationFoldersAsync(SynchronizationOptions options);
Task MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
@@ -179,6 +181,9 @@ namespace Wino.Core.Integration.Processors
=> AccountService.UpdateAccountAsync(account);
public Task UpdateAccountAliasesAsync(Guid accountId, List aliases)
- => AccountService.UpdateAccountAliases(accountId, aliases);
+ => AccountService.UpdateAccountAliasesAsync(accountId, aliases);
+
+ public Task> GetAccountAliasesAsync(Guid accountId)
+ => AccountService.GetAccountAliasesAsync(accountId);
}
}
diff --git a/Wino.Core/MenuItems/AccountMenuItem.cs b/Wino.Core/MenuItems/AccountMenuItem.cs
index 5cba23d3..c06da2f5 100644
--- a/Wino.Core/MenuItems/AccountMenuItem.cs
+++ b/Wino.Core/MenuItems/AccountMenuItem.cs
@@ -50,7 +50,7 @@ namespace Wino.Core.MenuItems
public string Base64ProfilePicture
{
get => Parameter.Name;
- set => SetProperty(Parameter.ProfilePictureBase64, value, Parameter, (u, n) => u.ProfilePictureBase64 = n);
+ set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n);
}
public IEnumerable HoldingAccounts => new List { Parameter };
@@ -65,7 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account;
AccountName = account.Name;
AttentionReason = account.AttentionReason;
- Base64ProfilePicture = account.ProfilePictureBase64;
+ Base64ProfilePicture = account.Base64ProfilePictureData;
if (SubMenuItems == null) return;
diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs
index ab665f30..e029e6bf 100644
--- a/Wino.Core/Services/AccountService.cs
+++ b/Wino.Core/Services/AccountService.cs
@@ -9,6 +9,7 @@ using SqlKata;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Extensions;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
@@ -226,43 +227,37 @@ namespace Wino.Core.Services
if (account.MergedInboxId != null)
account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value);
- // Load aliases
- account.Aliases = await GetAccountAliases(account.Id, account.Address);
-
account.Preferences = await GetAccountPreferencesAsync(account.Id);
}
return accounts;
}
- private async Task> GetAccountAliases(Guid accountId, string primaryAccountAddress)
+ public async Task CreateRootAliasAsync(Guid accountId, string address)
{
- // By default all accounts must have at least 1 primary alias to create drafts for.
- // If there's no alias, create one from the existing account address. Migration doesn't exists to create one for older messages.
-
- var aliases = await Connection
- .Table()
- .Where(a => a.AccountId == accountId)
- .ToListAsync()
- .ConfigureAwait(false);
-
- if (!aliases.Any())
+ var rootAlias = new MailAccountAlias()
{
- var primaryAccountAlias = new MailAccountAlias()
- {
- Id = Guid.NewGuid(),
- AccountId = accountId,
- IsPrimary = true,
- AliasAddress = primaryAccountAddress,
- ReplyToAddress = primaryAccountAddress,
- IsVerified = true,
- };
+ AccountId = accountId,
+ AliasAddress = address,
+ IsPrimary = true,
+ IsRootAlias = true,
+ IsVerified = true,
+ ReplyToAddress = address,
+ Id = Guid.NewGuid()
+ };
- await Connection.InsertAsync(primaryAccountAlias).ConfigureAwait(false);
- aliases.Add(primaryAccountAlias);
- }
+ await Connection.InsertAsync(rootAlias).ConfigureAwait(false);
- return aliases;
+ Log.Information("Created root alias for the account {AccountId}", accountId);
+ }
+
+ public async Task> GetAccountAliasesAsync(Guid accountId)
+ {
+ var query = new Query(nameof(MailAccountAlias))
+ .Where(nameof(MailAccountAlias.AccountId), accountId)
+ .OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
+
+ return await Connection.QueryAsync(query.GetRawQuery()).ConfigureAwait(false);
}
private Task GetMergedInboxInformationAsync(Guid mergedInboxId)
@@ -277,6 +272,7 @@ namespace Wino.Core.Services
await Connection.Table().Where(a => a.AccountId == account.Id).DeleteAsync();
await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id);
+ await Connection.Table().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.
@@ -327,6 +323,19 @@ namespace Wino.Core.Services
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;
+
+ await UpdateAccountAsync(account).ConfigureAwait(false);
+ }
+ }
+
public async Task GetAccountAsync(Guid accountId)
{
var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId);
@@ -359,7 +368,7 @@ namespace Wino.Core.Services
ReportUIChange(new AccountUpdatedMessage(account));
}
- public async Task UpdateAccountAliases(Guid accountId, List aliases)
+ public async Task UpdateAccountAliasesAsync(Guid accountId, List aliases)
{
// Delete existing ones.
await Connection.Table().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
@@ -371,6 +380,17 @@ namespace Wino.Core.Services
}
}
+ public async Task DeleteAccountAliasAsync(Guid aliasId)
+ {
+ // Create query to delete alias.
+
+ var query = new Query("MailAccountAlias")
+ .Where("Id", aliasId)
+ .AsDelete();
+
+ await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
+ }
+
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
{
Guard.IsNotNull(account);
@@ -424,7 +444,7 @@ namespace Wino.Core.Services
// Outlook token cache is managed by MSAL.
// Don't save it to database.
- if (tokenInformation != null && account.ProviderType != MailProviderType.Outlook)
+ if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
await Connection.InsertAsync(tokenInformation);
}
diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs
index 055a4bb4..11c107c6 100644
--- a/Wino.Core/Synchronizers/BaseSynchronizer.cs
+++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs
@@ -13,6 +13,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration;
@@ -71,10 +72,16 @@ namespace Wino.Core.Synchronizers
public abstract Task ExecuteNativeRequestsAsync(IEnumerable> batchedRequests, CancellationToken cancellationToken = default);
///
- /// Refreshed remote mail account profile if possible.
- /// Aliases, profile pictures, mailbox settings will be handled in this step.
+ /// Refreshes remote mail account profile if possible.
+ /// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
///
- protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask;
+ public virtual Task SynchronizeProfileInformationAsync() => default;
+
+ ///
+ /// Refreshes the aliases of the account.
+ /// Only available for Gmail right now.
+ ///
+ protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
///
/// Returns the base64 encoded profile picture of the account from the given URL.
@@ -100,6 +107,33 @@ namespace Wino.Core.Synchronizers
/// Synchronization result that contains summary of the sync.
protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+ ///
+ /// Safely updates account's profile information.
+ /// Database changes are reflected after this call.
+ /// Null returns mean that the operation failed.
+ ///
+ private async Task SynchronizeProfileInformationInternalAsync()
+ {
+ try
+ {
+ var profileInformation = await SynchronizeProfileInformationAsync();
+
+ if (profileInformation != null)
+ {
+ Account.SenderName = profileInformation.SenderName;
+ Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
+ }
+
+ return profileInformation;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to update profile information for account '{Name}'", Account.Name);
+ }
+
+ return null;
+ }
+
///
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
///
@@ -139,12 +173,16 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
- if (options.Type == SynchronizationType.Full)
+ if (options.Type == SynchronizationType.UpdateProfile)
{
- // Refresh profile information and mailbox settings on full synchronization.
+ // Refresh profile information on full synchronization.
// Exceptions here is not critical. Therefore, they are ignored.
- await SynchronizeProfileInformationAsync();
+ var newprofileInformation = await SynchronizeProfileInformationInternalAsync();
+
+ if (newprofileInformation == null) return SynchronizationResult.Failed;
+
+ return SynchronizationResult.Completed(null, newprofileInformation);
}
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index 57da147d..8dd48eab 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -19,6 +19,7 @@ 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.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -68,67 +69,45 @@ namespace Wino.Core.Synchronizers
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
- protected override async Task SynchronizeProfileInformationAsync()
+ public override async Task SynchronizeProfileInformationAsync()
{
- // Gmail profile info synchronizes Sender Name, Alias and Profile Picture.
+ var profileRequest = _peopleService.People.Get("people/me");
+ profileRequest.PersonFields = "names,photos";
- try
+ string senderName = string.Empty, base64ProfilePicture = string.Empty;
+
+ var userProfile = await profileRequest.ExecuteAsync();
+
+ senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
+
+ var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
+
+ if (!string.IsNullOrEmpty(profilePicture))
{
- var profileRequest = _peopleService.People.Get("people/me");
- profileRequest.PersonFields = "names,photos";
-
- string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64;
-
- var userProfile = await profileRequest.ExecuteAsync();
-
- senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
-
- var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
-
- if (!string.IsNullOrEmpty(profilePicture))
- {
- base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
- }
-
- bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
- || (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture);
-
- if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
- {
- Account.SenderName = senderName;
- }
-
- if (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture)
- {
- Account.ProfilePictureBase64 = base64ProfilePicture;
- }
-
- // Sync aliases
-
- var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
- var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
-
- var updatedAliases = sendAsListResponse.GetMailAliases(Account);
-
- bool shouldUpdateAliases =
- Account.Aliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) ||
- updatedAliases.Any(a => Account.Aliases.Any(b => a.Id == b.Id) == false);
-
- if (shouldUpdateAliases)
- {
- Account.Aliases = updatedAliases;
-
- await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases);
- }
-
- if (shouldUpdateAccountProfile)
- {
- await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false);
- }
+ base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
}
- catch (Exception ex)
+
+ return new ProfileInformation(senderName, base64ProfilePicture);
+ }
+
+ protected override async Task SynchronizeAliasesAsync()
+ {
+ // Sync aliases
+
+ var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
+ var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
+
+ var localAliases = await _gmailChangeProcessor.GetAccountAliasesAsync(Account.Id).ConfigureAwait(false);
+
+ var updatedAliases = sendAsListResponse.GetMailAliases(localAliases, Account);
+
+ bool shouldUpdateAliases =
+ localAliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) ||
+ updatedAliases.Any(a => localAliases.Any(b => a.Id == b.Id) == false);
+
+ if (shouldUpdateAliases)
{
- Logger.Error(ex, "Error while synchronizing profile information for {Name}", Account.Name);
+ await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases);
}
}
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 7080e9c7..90bc66f5 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -23,6 +23,7 @@ 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.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -479,70 +480,28 @@ namespace Wino.Core.Synchronizers
/// Base64 encoded profile picture.
private async Task GetUserProfilePictureAsync()
{
- try
- {
- var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
+ var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
- using var memoryStream = new MemoryStream();
- await photoStream.CopyToAsync(memoryStream);
- var byteArray = memoryStream.ToArray();
+ using var memoryStream = new MemoryStream();
+ await photoStream.CopyToAsync(memoryStream);
+ var byteArray = memoryStream.ToArray();
- return Convert.ToBase64String(byteArray);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error occurred while getting user profile picture.");
- return string.Empty;
- }
+ return Convert.ToBase64String(byteArray);
}
private async Task GetSenderNameAsync()
{
- try
- {
- var userInfo = await _graphClient.Users["me"].GetAsync();
+ var userInfo = await _graphClient.Users["me"].GetAsync();
- return userInfo.DisplayName;
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Failed to get sender name.");
- return string.Empty;
- }
+ return userInfo.DisplayName;
}
- protected override async Task SynchronizeProfileInformationAsync()
+ public override async Task SynchronizeProfileInformationAsync()
{
- try
- {
- // Outlook profile info synchronizes Sender Name and Profile Picture.
- string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64;
+ var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
+ var senderName = await GetSenderNameAsync().ConfigureAwait(false);
- var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
- senderName = await GetSenderNameAsync().ConfigureAwait(false);
-
- bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
- || (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != base64ProfilePicture);
-
- if (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != profilePictureData)
- {
- Account.ProfilePictureBase64 = profilePictureData;
- }
-
- if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
- {
- Account.SenderName = senderName;
- }
-
- if (shouldUpdateAccountProfile)
- {
- await _outlookChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Failed to synchronize profile information for {Name}", Account.Name);
- }
+ return new ProfileInformation(senderName, profilePictureData);
}
#region Mail Integration
diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
index eb8e0fc0..306f3bff 100644
--- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
+++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
@@ -56,7 +56,11 @@ namespace Wino.Mail.ViewModels
[RelayCommand]
private void EditSignature()
- => Messenger.Send(new BreadcrumbNavigationRequested("Signature", WinoPage.SignatureManagementPage, Account.Id));
+ => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsSignature_Title, WinoPage.SignatureManagementPage, Account.Id));
+
+ [RelayCommand]
+ private void EditAliases()
+ => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled)
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);
diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
index e503b99b..367ca141 100644
--- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs
+++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
@@ -154,15 +154,12 @@ namespace Wino.Mail.ViewModels
{
creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
- // _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType);
-
CustomServerInformation customServerInformation = null;
createdAccount = new MailAccount()
{
ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName,
- SenderName = accountCreationDialogResult.SenderName,
AccountColorHex = accountCreationDialogResult.AccountColorHex,
Id = Guid.NewGuid()
};
@@ -208,30 +205,59 @@ namespace Wino.Mail.ViewModels
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
// Local account has been created.
- // Create new synchronizer and start synchronization.
+
+ // Start profile information synchronization.
+ // Profile info is not updated in the database yet.
+
+ var profileSyncOptions = new SynchronizationOptions()
+ {
+ AccountId = createdAccount.Id,
+ Type = SynchronizationType.UpdateProfile
+ };
+
+ var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
+
+ var profileSynchronizationResult = profileSynchronizationResponse.Data;
+
+ if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
+
+ createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
+ createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
+
+ await _accountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog)
customServerAccountCreationDialog.ShowPreparingFolders();
else
creationDialog.State = AccountCreationDialogState.PreparingFolders;
- var options = new SynchronizationOptions()
+ // Start synchronizing folders.
+ var folderSyncOptions = new SynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = SynchronizationType.FoldersOnly
};
- var synchronizationResultResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(options, SynchronizationSource.Client));
+ var folderSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client));
- var synchronizationResult = synchronizationResultResponse.Data;
- if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success)
+ var folderSynchronizationResult = folderSynchronizationResponse.Data;
+
+ if (folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
- // Check if Inbox folder is available for the account after synchronization.
- var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
+ // Create root primary alias for the account.
+ // This is the first alias for the account and it's primary.
- if (!isInboxAvailable)
- throw new Exception(Translator.Exception_InboxNotAvailable);
+ await _accountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
+
+ // TODO: Temporary disabled. Is this even needed? Users can configure special folders manually later on if discovery fails.
+ // Check if Inbox folder is available for the account after synchronization.
+
+ //var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
+
+ //if (!isInboxAvailable)
+ // throw new Exception(Translator.Exception_InboxNotAvailable);
// Send changes to listeners.
ReportUIChange(new AccountCreatedMessage(createdAccount));
diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
new file mode 100644
index 00000000..234cf49f
--- /dev/null
+++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using EmailValidation;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+
+namespace Wino.Mail.ViewModels
+{
+ public partial class AliasManagementPageViewModel : BaseViewModel
+ {
+ private readonly IAccountService _accountService;
+
+ public MailAccount Account { get; set; }
+
+ [ObservableProperty]
+ private List accountAliases = [];
+
+ public AliasManagementPageViewModel(IDialogService dialogService, IAccountService accountService) : base(dialogService)
+ {
+ _accountService = accountService;
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ if (parameters is Guid accountId)
+ Account = await _accountService.GetAccountAsync(accountId);
+
+ if (Account == null) return;
+
+ await LoadAliasesAsync();
+ }
+
+ private async Task LoadAliasesAsync()
+ {
+ AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id);
+ }
+
+ [RelayCommand]
+ private async Task SetAliasPrimaryAsync(MailAccountAlias alias)
+ {
+ if (alias.IsPrimary) return;
+
+ AccountAliases.ForEach(a =>
+ {
+ a.IsPrimary = a == alias;
+ });
+
+ await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
+ await LoadAliasesAsync();
+ }
+
+ [RelayCommand]
+ private async Task AddNewAliasAsync()
+ {
+ var createdAliasDialog = await DialogService.ShowCreateAccountAliasDialogAsync();
+
+ if (createdAliasDialog.CreatedAccountAlias == null) return;
+
+ var newAlias = createdAliasDialog.CreatedAccountAlias;
+
+ // Check existence.
+ if (AccountAliases.Any(a => a.AliasAddress == newAlias.AliasAddress))
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, Translator.DialogMessage_AliasExistsMessage);
+ return;
+ }
+
+ // Validate all addresses.
+ if (!EmailValidator.Validate(newAlias.ReplyToAddress) || !EmailValidator.Validate(newAlias.AliasAddress))
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, Translator.DialogMessage_InvalidAliasTitle);
+ return;
+ }
+
+ newAlias.AccountId = Account.Id;
+
+ AccountAliases.Add(newAlias);
+
+ await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
+ DialogService.InfoBarMessage(Translator.DialogMessage_AliasCreatedTitle, Translator.DialogMessage_AliasCreatedMessage, InfoBarMessageType.Success);
+
+ await LoadAliasesAsync();
+ }
+
+ [RelayCommand]
+ private async Task DeleteAliasAsync(MailAccountAlias alias)
+ {
+ // Primary aliases can't be deleted.
+ if (alias.IsPrimary)
+ {
+ await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, Translator.GeneralTitle_Warning);
+ return;
+ }
+
+ // Root aliases can't be deleted.
+ if (alias.IsRootAlias)
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, Translator.DialogMessage_CantDeleteRootAliasMessage);
+ return;
+ }
+
+ await _accountService.DeleteAccountAliasAsync(alias.Id);
+ await LoadAliasesAsync();
+ }
+ }
+}
diff --git a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
index c90b81aa..9ba7f782 100644
--- a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
@@ -3,31 +3,21 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels.Data
{
- public class BreadcrumbNavigationItemViewModel : ObservableObject
+ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
{
+ [ObservableProperty]
+ private string title;
+
+ [ObservableProperty]
+ private bool isActive;
+
public BreadcrumbNavigationRequested Request { get; set; }
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
{
Request = request;
Title = request.PageTitle;
-
- this.isActive = isActive;
- }
-
- private string title;
- public string Title
- {
- get => title;
- set => SetProperty(ref title, value);
- }
-
- private bool isActive;
-
- public bool IsActive
- {
- get => isActive;
- set => SetProperty(ref isActive, value);
+ IsActive = isActive;
}
}
}
diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
index 946a3bf8..82c61360 100644
--- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
+++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
@@ -7,6 +7,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml
index 64b6c59e..57cb3f20 100644
--- a/Wino.Mail/App.xaml
+++ b/Wino.Mail/App.xaml
@@ -1,9 +1,10 @@
-
+
@@ -48,6 +49,14 @@
+
+
+
-
+