diff --git a/Wino.Core.Domain/Entities/MailCopy.cs b/Wino.Core.Domain/Entities/MailCopy.cs index 489bc916..eeb81ccd 100644 --- a/Wino.Core.Domain/Entities/MailCopy.cs +++ b/Wino.Core.Domain/Entities/MailCopy.cs @@ -141,7 +141,7 @@ namespace Wino.Core.Domain.Entities /// [Ignore] public MailAccount AssignedAccount { get; set; } - public IEnumerable GetContainingIds() => new[] { UniqueId }; + public IEnumerable GetContainingIds() => [UniqueId]; public override string ToString() => $"{Subject} <-> {Id}"; } } diff --git a/Wino.Core.Domain/Exceptions/MissingAliasException.cs b/Wino.Core.Domain/Exceptions/MissingAliasException.cs new file mode 100644 index 00000000..f81cb8c6 --- /dev/null +++ b/Wino.Core.Domain/Exceptions/MissingAliasException.cs @@ -0,0 +1,7 @@ +namespace Wino.Core.Domain.Exceptions +{ + public class MissingAliasException : System.Exception + { + public MissingAliasException() : base(Translator.Exception_MissingAlias) { } + } +} diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index 8c800220..128dac82 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -146,5 +146,14 @@ namespace Wino.Core.Domain.Interfaces /// Remotely fetched basic alias info from synchronizer. /// Account to update remote aliases for.. Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases); + + /// + /// Gets the primary account alias for the given account id. + /// Used when creating draft messages. + /// + /// Account id. + /// Primary alias for the account. + Task GetPrimaryAccountAliasAsync(Guid accountId); + } } diff --git a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs index 41f7a17b..f6b0076c 100644 --- a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs +++ b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs @@ -7,19 +7,22 @@ namespace Wino.Core.Domain.Models.MailItem { public class SendDraftPreparationRequest { - public MailCopy MailItem { get; set; } - public string Base64MimeMessage { get; set; } - public MailItemFolder SentFolder { get; set; } - public MailItemFolder DraftFolder { get; set; } - public MailAccountPreferences AccountPreferences { get; set; } + public MailCopy MailItem { get; } + public string Base64MimeMessage { get; } + public MailItemFolder SentFolder { get; } + public MailItemFolder DraftFolder { get; } + public MailAccountPreferences AccountPreferences { get; } + public MailAccountAlias SendingAlias { get; set; } public SendDraftPreparationRequest(MailCopy mailItem, + MailAccountAlias sendingAlias, MailItemFolder sentFolder, MailItemFolder draftFolder, MailAccountPreferences accountPreferences, string base64MimeMessage) { MailItem = mailItem; + SendingAlias = sendingAlias; SentFolder = sentFolder; DraftFolder = draftFolder; AccountPreferences = accountPreferences; diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 60c80324..7f61605a 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -67,6 +67,8 @@ "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_AliasNotSelectedTitle": "Missing Alias", + "DialogMessage_AliasNotSelectedMessage": "You must select an alias before sending a message.", "DialogMessage_AliasExistsTitle": "Existing Alias", "DialogMessage_AliasExistsMessage": "This alias is already in use.", "DialogMessage_InvalidAliasTitle": "Invalid Alias", @@ -133,6 +135,7 @@ "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", "Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index f3d42df0..fa0c50f4 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -358,6 +358,16 @@ namespace Wino.Core.Domain /// public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle"); + /// + /// Missing Alias + /// + public static string DialogMessage_AliasNotSelectedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedTitle"); + + /// + /// You must select an alias before sending a message. + /// + public static string DialogMessage_AliasNotSelectedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedMessage"); + /// /// Existing Alias /// @@ -688,6 +698,11 @@ namespace Wino.Core.Domain /// public static string Exception_FailedToSynchronizeAliases => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeAliases"); + /// + /// Primary alias does not exist for this account. Creating draft failed. + /// + public static string Exception_MissingAlias => Resources.GetTranslatedString(@"Exception_MissingAlias"); + /// /// Failed to synchronize profile information /// diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index 163c099e..e4e504e8 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -559,5 +559,14 @@ namespace Wino.Core.Services Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair)); } + + public async Task GetPrimaryAccountAliasAsync(Guid accountId) + { + var aliases = await GetAccountAliasesAsync(accountId); + + if (aliases == null || aliases.Count == 0) return null; + + return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First(); + } } } diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs index 780e4fbc..1d9381dd 100644 --- a/Wino.Core/Services/MailService.cs +++ b/Wino.Core/Services/MailService.cs @@ -10,6 +10,7 @@ using SqlKata; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Comparers; @@ -62,12 +63,14 @@ namespace Wino.Core.Services // This header will be used to map the local draft copy with the remote draft copy. var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader]; + var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(accountId).ConfigureAwait(false); + 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, + FromAddress = primaryAlias?.AliasAddress ?? composerAccount.Address, FromName = composerAccount.SenderName, HasAttachments = false, Importance = MailImportance.Normal, @@ -621,12 +624,17 @@ namespace Wino.Core.Services // 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 message = new MimeMessage() { Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } }, - From = { new MailboxAddress(account.SenderName, account.Address) } }; + var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(account.Id) ?? throw new MissingAliasException(); + + // Set FromName and FromAddress by alias. + message.From.Add(new MailboxAddress(account.SenderName, primaryAlias.AliasAddress)); + var builder = new BodyBuilder(); var signature = await GetSignature(account, draftCreationOptions.Reason); diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 38c78e07..4a9ac7cd 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -729,17 +729,26 @@ namespace Wino.Mail.ViewModels operationAccount = accounts.FirstOrDefault(); else { - // There are multiple accounts and there is no selection. - // Don't list all accounts, but only accounts that belong to Merged Inbox. - if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem) { + // There are multiple accounts and there is no selection. + // Don't list all accounts, but only accounts that belong to Merged Inbox. + var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId); if (!mergedAccounts.Any()) return; Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList())); } + else if (latestSelectedAccountMenuItem is AccountMenuItem selectedAccountMenuItem) + { + operationAccount = selectedAccountMenuItem.HoldingAccounts.ElementAt(0); + } + else + { + // User is at some other page. List all accounts. + Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(accounts)); + } } } diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 051672b1..983ed09a 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -68,6 +68,12 @@ namespace Wino.Mail.ViewModels [NotifyCanExecuteChangedFor(nameof(SendCommand))] private MailAccount composingAccount; + [ObservableProperty] + private List availableAliases; + + [ObservableProperty] + private MailAccountAlias selectedAlias; + [ObservableProperty] private bool isDraggingOverComposerGrid; @@ -166,6 +172,12 @@ namespace Wino.Mail.ViewModels if (!isConfirmed) return; } + if (SelectedAlias == null) + { + DialogService.InfoBarMessage(Translator.DialogMessage_AliasNotSelectedTitle, Translator.DialogMessage_AliasNotSelectedMessage, InfoBarMessageType.Error); + return; + } + // Save mime changes before sending. await UpdateMimeChangesAsync().ConfigureAwait(false); @@ -180,7 +192,12 @@ namespace Wino.Mail.ViewModels int count = (int)memoryStream.Length; var base64EncodedMessage = Convert.ToBase64String(buffer); - var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, sentFolder, CurrentMailDraftItem.AssignedFolder, CurrentMailDraftItem.AssignedAccount.Preferences, base64EncodedMessage); + var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, + SelectedAlias, + sentFolder, + CurrentMailDraftItem.AssignedFolder, + CurrentMailDraftItem.AssignedAccount.Preferences, + base64EncodedMessage); await _worker.ExecuteAsync(draftSendPreparationRequest); } @@ -197,6 +214,8 @@ namespace Wino.Mail.ViewModels SaveImportance(); SaveSubject(); + SaveFromAddress(); + SaveReplyToAddress(); await SaveAttachmentsAsync(); await SaveBodyAsync(); @@ -210,6 +229,7 @@ namespace Wino.Mail.ViewModels { CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject; CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody; + CurrentMailDraftItem.FromAddress = SelectedAlias.AliasAddress; // Update database. await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy); @@ -227,7 +247,10 @@ namespace Wino.Mail.ViewModels } } - private void SaveImportance() { CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal; } + private void SaveImportance() + { + CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal; + } private void SaveSubject() { @@ -285,14 +308,12 @@ namespace Wino.Mail.ViewModels await UpdateMimeChangesAsync().ConfigureAwait(false); } - public override async void OnNavigatedTo(NavigationMode mode, object parameters) + public override void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); if (parameters != null && parameters is MailItemViewModel mailItem) { - await LoadAccountsAsync(); - CurrentMailDraftItem = mailItem; _ = TryPrepareComposeAsync(true); @@ -321,46 +342,52 @@ namespace Wino.Mail.ViewModels } } - private async Task LoadAccountsAsync() - { - // Load accounts - - var accounts = await _accountService.GetAccountsAsync(); - - foreach (var account in accounts) - { - Accounts.Add(account); - } - } - private async Task InitializeComposerAccountAsync() { + if (CurrentMailDraftItem == null) return false; + if (ComposingAccount != null) return true; - if (CurrentMailDraftItem == null) - return false; + var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false); + if (composingAccount == null) return false; + + var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false); + + if (aliases == null || !aliases.Any()) return false; + + // MailAccountAlias primaryAlias = aliases.Find(a => a.IsPrimary) ?? aliases.First(); + + // Auto-select the correct alias from the message itself. + // If can't, fallback to primary alias. + + MailAccountAlias primaryAlias = null; + + if (!string.IsNullOrEmpty(CurrentMailDraftItem.FromAddress)) + { + primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress); + } + + primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false); await ExecuteUIThread(() => { - ComposingAccount = Accounts.FirstOrDefault(a => a.Id == CurrentMailDraftItem.AssignedAccount.Id); + ComposingAccount = composingAccount; + AvailableAliases = aliases; + SelectedAlias = primaryAlias; }); - return ComposingAccount != null; + return true; } private async Task TryPrepareComposeAsync(bool downloadIfNeeded) { - if (CurrentMailDraftItem == null) - return; + if (CurrentMailDraftItem == null) return; bool isComposerInitialized = await InitializeComposerAccountAsync(); - if (!isComposerInitialized) - { - return; - } + if (!isComposerInitialized) return; - retry: + retry: // Replying existing message. MimeMessageInformation mimeMessageInformation = null; @@ -452,6 +479,31 @@ namespace Wino.Mail.ViewModels } } + private void SaveFromAddress() + { + if (SelectedAlias == null || CurrentMimeMessage == null) return; + + + CurrentMimeMessage.From.Clear(); + CurrentMimeMessage.From.Add(new MailboxAddress(ComposingAccount.SenderName, SelectedAlias.AliasAddress)); + + + } + + private void SaveReplyToAddress() + { + if (SelectedAlias == null || CurrentMimeMessage == null) return; + + if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress)) + { + if (!CurrentMimeMessage.ReplyTo.Any(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == SelectedAlias.ReplyToAddress)) + { + CurrentMimeMessage.ReplyTo.Clear(); + CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress)); + } + } + } + private void SaveAddressInfo(IEnumerable addresses, InternetAddressList list) { list.Clear(); diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index aa77ead0..3caf3398 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -18,7 +18,6 @@ namespace Wino.Mail.ViewModels.Data public string MessageId => ((IMailItem)MailCopy).MessageId; public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress; public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate; - public string FromAddress => ((IMailItem)MailCopy).FromAddress; public bool HasAttachments => ((IMailItem)MailCopy).HasAttachments; public string References => ((IMailItem)MailCopy).References; public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; @@ -77,6 +76,12 @@ namespace Wino.Mail.ViewModels.Data set => SetProperty(MailCopy.PreviewText, value, MailCopy, (u, n) => u.PreviewText = n); } + public string FromAddress + { + get => MailCopy.FromAddress; + set => SetProperty(MailCopy.FromAddress, value, MailCopy, (u, n) => u.FromAddress = n); + } + public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder; public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount; diff --git a/Wino.Mail/Views/ComposePage.xaml b/Wino.Mail/Views/ComposePage.xaml index f214392d..b7a10162 100644 --- a/Wino.Mail/Views/ComposePage.xaml +++ b/Wino.Mail/Views/ComposePage.xaml @@ -1,23 +1,22 @@ - + @@ -30,11 +29,10 @@ - + @@ -43,10 +41,9 @@ - + @@ -56,11 +53,10 @@ - + @@ -68,61 +64,54 @@ - + - + - + - + - + - + - + Transparent @@ -135,36 +124,32 @@ - + - + - + - + @@ -183,10 +168,9 @@ - + @@ -199,39 +183,34 @@ - - + + - + - + - + @@ -239,19 +218,17 @@ - + - + @@ -259,10 +236,9 @@ - + @@ -270,10 +246,9 @@ - + @@ -282,12 +257,11 @@ - + @@ -328,11 +302,10 @@ - + @@ -340,33 +313,29 @@ - - + + - + - + @@ -414,11 +383,10 @@ - + @@ -434,149 +402,128 @@ - + - - - - - - - - - + - + - + - + - - + - + - + - + - + - + - + @@ -586,72 +533,65 @@ - + - - + + - + - + - - + +