From 3bd0b69429aa83de75f9631096b4fe5f46da6339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sun, 19 Apr 2026 20:13:09 +0200 Subject: [PATCH] Imap flow. --- .../ProviderSelectionNavigationContext.cs | 26 +++++ .../Settings/SettingsNavigationItemInfo.cs | 7 +- .../Translations/en_US/resources.json | 6 +- .../Validation/MailAccountAddressValidator.cs | 64 +++++++++++++ .../Services/AutoDiscoveryServiceTests.cs | 35 +++++++ .../SettingOptionsPageViewModel.cs | 5 +- Wino.Core/Integration/ImapClientPool.cs | 11 ++- Wino.Core/Services/AutoDiscoveryService.cs | 4 + Wino.Core/Services/SynchronizationManager.cs | 2 +- .../AccountManagementViewModel.cs | 5 +- .../ImapCalDavSettingsNavigationContext.cs | 11 ++- .../ImapCalDavSettingsPageViewModel.cs | 96 +++++++++++++++++-- .../ProviderSelectionPageViewModel.cs | 19 +++- .../SpecialImapCredentialsPageViewModel.cs | 3 +- .../WelcomePageV2ViewModel.cs | 4 +- .../Dialogs/NewAccountDialog.xaml.cs | 3 +- 16 files changed, 277 insertions(+), 24 deletions(-) create mode 100644 Wino.Core.Domain/Models/Navigation/ProviderSelectionNavigationContext.cs create mode 100644 Wino.Core.Domain/Validation/MailAccountAddressValidator.cs diff --git a/Wino.Core.Domain/Models/Navigation/ProviderSelectionNavigationContext.cs b/Wino.Core.Domain/Models/Navigation/ProviderSelectionNavigationContext.cs new file mode 100644 index 00000000..d280f20c --- /dev/null +++ b/Wino.Core.Domain/Models/Navigation/ProviderSelectionNavigationContext.cs @@ -0,0 +1,26 @@ +namespace Wino.Core.Domain.Models.Navigation; + +public enum ProviderSelectionHostMode +{ + Wizard, + SettingsAddAccount +} + +public sealed class ProviderSelectionNavigationContext +{ + public ProviderSelectionHostMode HostMode { get; init; } = ProviderSelectionHostMode.Wizard; + + public static ProviderSelectionNavigationContext CreateForWizard() + => new() + { + HostMode = ProviderSelectionHostMode.Wizard + }; + + public static ProviderSelectionNavigationContext CreateForSettingsAddAccount() + => new() + { + HostMode = ProviderSelectionHostMode.SettingsAddAccount + }; + + public bool IsWizardHost => HostMode == ProviderSelectionHostMode.Wizard; +} diff --git a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs index f8f2beab..6c135fa0 100644 --- a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs +++ b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs @@ -141,7 +141,9 @@ public static class SettingsNavigationInfoProvider public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "") { var rootPage = GetRootPage(pageType); - return GetNavigationItems(manageAccountsDescription).First(item => item.PageType == rootPage); + return GetNavigationItems(manageAccountsDescription) + .FirstOrDefault(item => item.PageType == rootPage) + ?? GetNavigationItems(manageAccountsDescription).First(item => item.PageType == WinoPage.SettingOptionsPage); } public static string GetPageTitle(WinoPage pageType) @@ -180,6 +182,9 @@ public static class SettingsNavigationInfoProvider WinoPage.MailCategoryManagementPage => WinoPage.ManageAccountsPage, WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage, WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage, + WinoPage.ProviderSelectionPage => WinoPage.ManageAccountsPage, + WinoPage.SpecialImapCredentialsPage => WinoPage.ManageAccountsPage, + WinoPage.AccountSetupProgressPage => WinoPage.ManageAccountsPage, WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage, WinoPage.CalendarSettingsPage => WinoPage.CalendarPreferenceSettingsPage, WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarPreferenceSettingsPage, diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 78fc4b1f..4f0e032c 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -438,7 +438,7 @@ "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required", "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required", "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Display name is required", - "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid email address", + "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid mailbox address, such as user@example.com or user@localhost", "IMAPAdvancedSetupDialog_ValidationEmailRequired": "Email address is required", "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:", "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535", @@ -485,7 +485,7 @@ "IMAPSetupDialog_IMAPSettings": "IMAP Server Settings", "IMAPSetupDialog_SMTPSettings": "SMTP Server Settings", "IMAPSetupDialog_MailAddress": "Email address", - "IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com", + "IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com or user@localhost", "IMAPSetupDialog_OutgoingMailServer": "Outgoing (SMTP) mail server", "IMAPSetupDialog_OutgoingMailServerPassword": "Outgoing server password", "IMAPSetupDialog_OutgoingMailServerPort": "Port", @@ -502,7 +502,7 @@ "ImapCalDavSettingsPage_TitleEdit": "Edit IMAP and Calendar Settings", "ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP and optional calendar synchronization for this account.", "ImapCalDavSettingsPage_BasicSectionTitle": "Basic setup", - "ImapCalDavSettingsPage_BasicSectionDescription": "Enter your identity and credentials. Wino can try to detect server settings automatically.", + "ImapCalDavSettingsPage_BasicSectionDescription": "Enter your identity and credentials. Wino supports manual addresses such as user@localhost and can try to detect server settings automatically.", "ImapCalDavSettingsPage_BasicTab": "Basic", "ImapCalDavSettingsPage_EnableCalendarSupport": "Enable calendar support", "ImapCalDavSettingsPage_AutoDiscoverButton": "Autodiscover mail settings", diff --git a/Wino.Core.Domain/Validation/MailAccountAddressValidator.cs b/Wino.Core.Domain/Validation/MailAccountAddressValidator.cs new file mode 100644 index 00000000..d90fb55e --- /dev/null +++ b/Wino.Core.Domain/Validation/MailAccountAddressValidator.cs @@ -0,0 +1,64 @@ +using System; +using System.Net.Mail; + +namespace Wino.Core.Domain.Validation; + +public static class MailAccountAddressValidator +{ + public static bool IsValid(string address) + { + if (string.IsNullOrWhiteSpace(address)) + return false; + + var trimmedAddress = address.Trim(); + + if (trimmedAddress.Contains('\r') || trimmedAddress.Contains('\n')) + return false; + + try + { + var parsedAddress = new MailAddress(trimmedAddress); + return parsedAddress.Address.Equals(trimmedAddress, StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + public static bool TryGetDomain(string address, out string domain) + { + domain = string.Empty; + + if (!IsValid(address)) + return false; + + var trimmedAddress = address.Trim(); + var separatorIndex = trimmedAddress.LastIndexOf('@'); + + if (separatorIndex <= 0 || separatorIndex >= trimmedAddress.Length - 1) + return false; + + domain = trimmedAddress[(separatorIndex + 1)..]; + return !string.IsNullOrWhiteSpace(domain); + } + + public static bool IsImplicitlyResolvableHost(string host) + { + if (string.IsNullOrWhiteSpace(host)) + return false; + + var normalizedHost = host.Trim().TrimEnd('.'); + if (string.IsNullOrWhiteSpace(normalizedHost)) + return false; + + if (normalizedHost.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + return true; + + var hostType = Uri.CheckHostName(normalizedHost); + if (hostType is UriHostNameType.IPv4 or UriHostNameType.IPv6) + return true; + + return normalizedHost.IndexOf('.') < 0; + } +} diff --git a/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs b/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs index fd1dd8c1..909856e2 100644 --- a/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs +++ b/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs @@ -150,6 +150,41 @@ public class AutoDiscoveryServiceTests uri.Should().Be(new Uri("https://dav.example.net/caldav/")); } + [Fact] + public async Task GetAutoDiscoverySettings_ReturnsGuessedLocalhostSettings_ForManualLocalAccounts() + { + var handler = new StubHttpMessageHandler(request => + { + var uri = request.RequestUri!.ToString(); + + if (uri.StartsWith("https://dns.google/resolve", StringComparison.OrdinalIgnoreCase)) + { + return CreateJsonResponse("{\"Status\":0}", request); + } + + return CreateStatusResponse(HttpStatusCode.NotFound, request); + }); + + using var client = new HttpClient(handler); + var sut = new AutoDiscoveryService(client); + + var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings + { + Email = "user@localhost", + DisplayName = "User", + Password = "secret" + }); + + settings.Should().NotBeNull(); + settings!.Domain.Should().Be("localhost"); + settings.GetImapSettings()!.Address.Should().Be("localhost"); + settings.GetImapSettings()!.Port.Should().Be(993); + settings.GetImapSettings()!.Username.Should().Be("user@localhost"); + settings.GetSmptpSettings()!.Address.Should().Be("localhost"); + settings.GetSmptpSettings()!.Port.Should().Be(587); + settings.GetSmptpSettings()!.Username.Should().Be("user@localhost"); + } + private static HttpResponseMessage CreateXmlResponse(string xml, HttpRequestMessage request) => new(HttpStatusCode.OK) { diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index b51d8109..d9e8a2b5 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -160,7 +160,10 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel public void NavigateToAddAccount() { Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); - Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage)); + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.WelcomeWizard_Step2Title, + WinoPage.ProviderSelectionPage, + ProviderSelectionNavigationContext.CreateForSettingsAddAccount())); } public void NavigateToManageAccounts() diff --git a/Wino.Core/Integration/ImapClientPool.cs b/Wino.Core/Integration/ImapClientPool.cs index 1c0c997d..3046c82f 100644 --- a/Wino.Core/Integration/ImapClientPool.cs +++ b/Wino.Core/Integration/ImapClientPool.cs @@ -58,6 +58,7 @@ public class ImapClientPool : IDisposable private readonly int _targetMinimumConnections; private DateTime _lastKeepAliveSentUtc = DateTime.MinValue; + private Exception _lastConnectionException; private WinoImapClient _dedicatedIdleClient; private bool _disposedValue; private bool _initialized; @@ -114,7 +115,7 @@ public class ImapClientPool : IDisposable var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); if (initialClient == null) { - throw CreatePoolException("Failed to create initial IMAP connection for the pool."); + throw CreatePoolException("Failed to create initial IMAP connection for the pool.", _lastConnectionException); } _clientStates[initialClient] = ImapClientState.Available; @@ -192,6 +193,7 @@ public class ImapClientPool : IDisposable } catch (Exception ex) { + _lastConnectionException = ex; _logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed."); MarkClientAsFailed(pooledClient); } @@ -215,12 +217,12 @@ public class ImapClientPool : IDisposable } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}."); + throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.", _lastConnectionException); } throw cancellationToken.IsCancellationRequested ? new OperationCanceledException(cancellationToken) - : CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}."); + : CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.", _lastConnectionException); } /// @@ -516,14 +518,17 @@ public class ImapClientPool : IDisposable private async Task CreateAndConnectClientAsync(CancellationToken cancellationToken) { var client = CreateNewClient(); + _lastConnectionException = null; try { await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false); + _lastConnectionException = null; return client; } catch (Exception ex) { + _lastConnectionException = ex; _logger.Warning(ex, "Failed to create and connect IMAP client."); DisposeClient(client); return null; diff --git a/Wino.Core/Services/AutoDiscoveryService.cs b/Wino.Core/Services/AutoDiscoveryService.cs index 10d21137..867006fe 100644 --- a/Wino.Core/Services/AutoDiscoveryService.cs +++ b/Wino.Core/Services/AutoDiscoveryService.cs @@ -11,6 +11,7 @@ using Serilog; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.AutoDiscovery; +using Wino.Core.Domain.Validation; namespace Wino.Core.Services; @@ -450,6 +451,9 @@ public class AutoDiscoveryService : IAutoDiscoveryService private async Task HasAnyDnsAddressRecordAsync(string host, CancellationToken cancellationToken) { + if (MailAccountAddressValidator.IsImplicitlyResolvableHost(host)) + return true; + var aRecords = await QueryDnsAsync(host, "A", cancellationToken).ConfigureAwait(false); if (aRecords.Count > 0) return true; diff --git a/Wino.Core/Services/SynchronizationManager.cs b/Wino.Core/Services/SynchronizationManager.cs index b570aea9..dde49cbc 100644 --- a/Wino.Core/Services/SynchronizationManager.cs +++ b/Wino.Core/Services/SynchronizationManager.cs @@ -123,7 +123,7 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient AddNewAccountAsync(); diff --git a/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs index 8a1bce39..904573c1 100644 --- a/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs +++ b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs @@ -10,7 +10,8 @@ public enum ImapCalDavSettingsPageMode { Create, Edit, - Wizard + Wizard, + AddAccount } public sealed class ImapCalDavSettingsNavigationContext @@ -45,6 +46,14 @@ public sealed class ImapCalDavSettingsNavigationContext AccountCreationDialogResult = accountCreationDialogResult }; + public static ImapCalDavSettingsNavigationContext CreateForAddAccountMode( + AccountCreationDialogResult accountCreationDialogResult) + => new() + { + Mode = ImapCalDavSettingsPageMode.AddAccount, + AccountCreationDialogResult = accountCreationDialogResult + }; + public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard; } diff --git a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs index 9164cabe..fe73cd8b 100644 --- a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs +++ b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs @@ -14,6 +14,7 @@ using Wino.Core.Domain.Models.AutoDiscovery; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Domain.Validation; using Wino.Core.Services; using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; @@ -135,7 +136,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel [NotifyPropertyChangedFor(nameof(IsAdvancedSetupSelected))] private int selectedSetupTabIndex; - public bool IsCreateMode => _pageMode == ImapCalDavSettingsPageMode.Create; + public bool IsCreateMode => _pageMode is ImapCalDavSettingsPageMode.Create or ImapCalDavSettingsPageMode.AddAccount; public bool IsEditMode => !IsCreateMode; public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint); public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0; @@ -289,7 +290,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel _localOnlyInfoShown = false; SelectedSetupTabIndex = 0; - if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard) + if (_pageMode is ImapCalDavSettingsPageMode.Create or ImapCalDavSettingsPageMode.Wizard or ImapCalDavSettingsPageMode.AddAccount) { PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate; ApplyCreateContextDefaults(context.AccountCreationDialogResult); @@ -412,7 +413,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel ? _editingAccountId : (Guid?)null; - if (!await ValidateAccountUniquenessAsync(excludedAccountId).ConfigureAwait(false)) + if (!await ValidateAccountUniquenessAsync(excludedAccountId)) return; await ValidateImapConnectivityAsync(serverInformation); @@ -434,6 +435,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel return; } + if (_pageMode == ImapCalDavSettingsPageMode.AddAccount) + { + CompleteAddAccountFlow(serverInformation); + return; + } + if (_pageMode == ImapCalDavSettingsPageMode.Create) { CompleteCreateFlow(serverInformation); @@ -464,6 +471,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel } private void CompleteWizardFlow(CustomServerInformation serverInformation) + => ContinueAccountCreationFlow(serverInformation); + + private void CompleteAddAccountFlow(CustomServerInformation serverInformation) + => ContinueAccountCreationFlow(serverInformation); + + private void ContinueAccountCreationFlow(CustomServerInformation serverInformation) { serverInformation.Id = Guid.NewGuid(); serverInformation.AccountId = Guid.Empty; @@ -512,6 +525,31 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel } } + partial void OnEmailAddressChanged(string oldValue, string newValue) + { + var previousAddress = oldValue?.Trim() ?? string.Empty; + var currentAddress = newValue?.Trim() ?? string.Empty; + + ApplyCredentialDefaultsForAddress(previousAddress, currentAddress); + ApplyManualServerDefaultsForAddress(previousAddress, currentAddress); + + IsImapValidationSucceeded = false; + IsCalDavValidationSucceeded = false; + } + + partial void OnPasswordChanged(string oldValue, string newValue) + { + var previousPassword = oldValue ?? string.Empty; + var currentPassword = newValue ?? string.Empty; + + IncomingServerPassword = ReplaceIfEmptyOrMatchingPrevious(IncomingServerPassword, previousPassword, currentPassword); + OutgoingServerPassword = ReplaceIfEmptyOrMatchingPrevious(OutgoingServerPassword, previousPassword, currentPassword); + CalDavPassword = ReplaceIfEmptyOrMatchingPrevious(CalDavPassword, previousPassword, currentPassword); + + IsImapValidationSucceeded = false; + IsCalDavValidationSucceeded = false; + } + private async Task InitializeEditModeAsync(Guid accountId) { var account = await _accountService.GetAccountAsync(accountId); @@ -618,6 +656,50 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel SelectedOutgoingServerAuthenticationMethodIndex = 0; } + private void ApplyCredentialDefaultsForAddress(string previousAddress, string currentAddress) + { + IncomingServerUsername = ReplaceIfEmptyOrMatchingPrevious(IncomingServerUsername, previousAddress, currentAddress); + OutgoingServerUsername = ReplaceIfEmptyOrMatchingPrevious(OutgoingServerUsername, previousAddress, currentAddress); + CalDavUsername = ReplaceIfEmptyOrMatchingPrevious(CalDavUsername, previousAddress, currentAddress); + } + + private void ApplyManualServerDefaultsForAddress(string previousAddress, string currentAddress) + { + if (!MailAccountAddressValidator.TryGetDomain(currentAddress, out var currentDomain) || + !MailAccountAddressValidator.IsImplicitlyResolvableHost(currentDomain)) + { + return; + } + + MailAccountAddressValidator.TryGetDomain(previousAddress, out var previousDomain); + + IncomingServer = ReplaceIfEmptyOrMatchingPrevious(IncomingServer, previousDomain, currentDomain); + OutgoingServer = ReplaceIfEmptyOrMatchingPrevious(OutgoingServer, previousDomain, currentDomain); + + if (string.IsNullOrWhiteSpace(IncomingServerPort)) + IncomingServerPort = "993"; + + if (string.IsNullOrWhiteSpace(OutgoingServerPort)) + OutgoingServerPort = "587"; + } + + private static string ReplaceIfEmptyOrMatchingPrevious(string currentValue, string previousValue, string replacementValue) + { + var normalizedCurrentValue = currentValue?.Trim() ?? string.Empty; + var normalizedPreviousValue = previousValue?.Trim() ?? string.Empty; + var normalizedReplacementValue = replacementValue?.Trim() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(normalizedReplacementValue)) + return currentValue ?? string.Empty; + + if (string.IsNullOrWhiteSpace(normalizedCurrentValue)) + return normalizedReplacementValue; + + return string.Equals(normalizedCurrentValue, normalizedPreviousValue, StringComparison.OrdinalIgnoreCase) + ? normalizedReplacementValue + : currentValue ?? string.Empty; + } + private void ApplyServerInformation(CustomServerInformation serverInformation) { if (serverInformation == null) @@ -809,7 +891,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel private async Task SaveEditFlowAsync(CustomServerInformation serverInformation) { - var account = await _accountService.GetAccountAsync(_editingAccountId).ConfigureAwait(false); + var account = await _accountService.GetAccountAsync(_editingAccountId); if (account == null) throw new InvalidOperationException(Translator.Exception_NullAssignedAccount); @@ -823,8 +905,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel account.ServerInformation = serverInformation; account.AttentionReason = AccountAttentionReason.None; - await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false); - await _accountService.UpdateAccountAsync(account).ConfigureAwait(false); + await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation); + await _accountService.UpdateAccountAsync(account); Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions { @@ -916,7 +998,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel if (string.IsNullOrWhiteSpace(EmailAddress)) throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailRequired); - if (!EmailValidation.EmailValidator.Validate(EmailAddress.Trim())) + if (!MailAccountAddressValidator.IsValid(EmailAddress)) throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailInvalid); } diff --git a/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs index 8d1dd0a9..35b0e75c 100644 --- a/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs +++ b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs @@ -21,6 +21,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel private readonly IDialogServiceBase _dialogService; private readonly IProviderService _providerService; private readonly INewThemeService _themeService; + private ProviderSelectionHostMode _hostMode = ProviderSelectionHostMode.Wizard; public WelcomeWizardContext WizardContext { get; } @@ -74,6 +75,16 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel { base.OnNavigatedTo(mode, parameters); + var navigationContext = parameters as ProviderSelectionNavigationContext + ?? ProviderSelectionNavigationContext.CreateForWizard(); + + _hostMode = navigationContext.HostMode; + + if (mode != NavigationMode.Back) + { + WizardContext.Reset(); + } + Providers = _providerService.GetAvailableProviders(); AvailableColors = _themeService.GetAvailableAccountColors() .Select(hex => new AppColorViewModel(hex)) @@ -135,9 +146,11 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel if (WizardContext.IsGenericImap) { - // Navigate to ImapCalDavSettingsPage in wizard mode - var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode( - WizardContext.BuildAccountCreationDialogResult()); + var context = _hostMode == ProviderSelectionHostMode.SettingsAddAccount + ? ImapCalDavSettingsNavigationContext.CreateForAddAccountMode( + WizardContext.BuildAccountCreationDialogResult()) + : ImapCalDavSettingsNavigationContext.CreateForWizardMode( + WizardContext.BuildAccountCreationDialogResult()); Messenger.Send(new BreadcrumbNavigationRequested( Translator.ImapCalDavSettingsPage_TitleCreate, diff --git a/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs index ccc55289..380bf0df 100644 --- a/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs +++ b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs @@ -8,6 +8,7 @@ using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Validation; using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; @@ -99,7 +100,7 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel { CanProceed = !string.IsNullOrWhiteSpace(DisplayName) && !string.IsNullOrWhiteSpace(EmailAddress) - && EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty) + && MailAccountAddressValidator.IsValid(EmailAddress) && !string.IsNullOrWhiteSpace(AppSpecificPassword); } diff --git a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs index 1784aa32..a3021e8d 100644 --- a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs +++ b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs @@ -13,6 +13,7 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Updates; +using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; using Wino.Messaging.UI; @@ -68,7 +69,8 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel { Messenger.Send(new BreadcrumbNavigationRequested( Translator.WelcomeWizard_Step2Title, - WinoPage.ProviderSelectionPage)); + WinoPage.ProviderSelectionPage, + ProviderSelectionNavigationContext.CreateForWizard())); } [RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))] diff --git a/Wino.Mail.WinUI/Dialogs/NewAccountDialog.xaml.cs b/Wino.Mail.WinUI/Dialogs/NewAccountDialog.xaml.cs index fd930596..2b6f2ead 100644 --- a/Wino.Mail.WinUI/Dialogs/NewAccountDialog.xaml.cs +++ b/Wino.Mail.WinUI/Dialogs/NewAccountDialog.xaml.cs @@ -8,6 +8,7 @@ using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Validation; using Wino.Core.ViewModels.Data; using Wino.Helpers; @@ -208,7 +209,7 @@ public sealed partial class NewAccountDialog : ContentDialog && !string.IsNullOrEmpty(AccountNameTextbox.Text) && (IsSpecialImapServerPartVisible ? (!string.IsNullOrEmpty(AppSpecificPassword.Password) && !string.IsNullOrEmpty(DisplayNameTextBox.Text) - && EmailValidation.EmailValidator.Validate(SpecialImapAddress.Text)) : true); + && MailAccountAddressValidator.IsValid(SpecialImapAddress.Text)) : true); IsPrimaryButtonEnabled = shouldEnable; }