Imap flow.

This commit is contained in:
Burak Kaan Köse
2026-04-19 20:13:09 +02:00
parent 496c7735f7
commit 3bd0b69429
16 changed files with 277 additions and 24 deletions
@@ -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;
}
@@ -141,7 +141,9 @@ public static class SettingsNavigationInfoProvider
public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "") public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "")
{ {
var rootPage = GetRootPage(pageType); 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) public static string GetPageTitle(WinoPage pageType)
@@ -180,6 +182,9 @@ public static class SettingsNavigationInfoProvider
WinoPage.MailCategoryManagementPage => WinoPage.ManageAccountsPage, WinoPage.MailCategoryManagementPage => WinoPage.ManageAccountsPage,
WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage, WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage,
WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage, WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage,
WinoPage.ProviderSelectionPage => WinoPage.ManageAccountsPage,
WinoPage.SpecialImapCredentialsPage => WinoPage.ManageAccountsPage,
WinoPage.AccountSetupProgressPage => WinoPage.ManageAccountsPage,
WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage, WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage,
WinoPage.CalendarSettingsPage => WinoPage.CalendarPreferenceSettingsPage, WinoPage.CalendarSettingsPage => WinoPage.CalendarPreferenceSettingsPage,
WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarPreferenceSettingsPage, WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarPreferenceSettingsPage,
@@ -438,7 +438,7 @@
"IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required", "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required",
"IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required", "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required",
"IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Display name 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_ValidationEmailRequired": "Email address is required",
"IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:", "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:",
"IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535", "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535",
@@ -485,7 +485,7 @@
"IMAPSetupDialog_IMAPSettings": "IMAP Server Settings", "IMAPSetupDialog_IMAPSettings": "IMAP Server Settings",
"IMAPSetupDialog_SMTPSettings": "SMTP Server Settings", "IMAPSetupDialog_SMTPSettings": "SMTP Server Settings",
"IMAPSetupDialog_MailAddress": "Email address", "IMAPSetupDialog_MailAddress": "Email address",
"IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com", "IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com or user@localhost",
"IMAPSetupDialog_OutgoingMailServer": "Outgoing (SMTP) mail server", "IMAPSetupDialog_OutgoingMailServer": "Outgoing (SMTP) mail server",
"IMAPSetupDialog_OutgoingMailServerPassword": "Outgoing server password", "IMAPSetupDialog_OutgoingMailServerPassword": "Outgoing server password",
"IMAPSetupDialog_OutgoingMailServerPort": "Port", "IMAPSetupDialog_OutgoingMailServerPort": "Port",
@@ -502,7 +502,7 @@
"ImapCalDavSettingsPage_TitleEdit": "Edit IMAP and Calendar Settings", "ImapCalDavSettingsPage_TitleEdit": "Edit IMAP and Calendar Settings",
"ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP and optional calendar synchronization for this account.", "ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP and optional calendar synchronization for this account.",
"ImapCalDavSettingsPage_BasicSectionTitle": "Basic setup", "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_BasicTab": "Basic",
"ImapCalDavSettingsPage_EnableCalendarSupport": "Enable calendar support", "ImapCalDavSettingsPage_EnableCalendarSupport": "Enable calendar support",
"ImapCalDavSettingsPage_AutoDiscoverButton": "Autodiscover mail settings", "ImapCalDavSettingsPage_AutoDiscoverButton": "Autodiscover mail settings",
@@ -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;
}
}
@@ -150,6 +150,41 @@ public class AutoDiscoveryServiceTests
uri.Should().Be(new Uri("https://dav.example.net/caldav/")); 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) private static HttpResponseMessage CreateXmlResponse(string xml, HttpRequestMessage request)
=> new(HttpStatusCode.OK) => new(HttpStatusCode.OK)
{ {
@@ -160,7 +160,10 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
public void NavigateToAddAccount() public void NavigateToAddAccount()
{ {
Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); 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() public void NavigateToManageAccounts()
+8 -3
View File
@@ -58,6 +58,7 @@ public class ImapClientPool : IDisposable
private readonly int _targetMinimumConnections; private readonly int _targetMinimumConnections;
private DateTime _lastKeepAliveSentUtc = DateTime.MinValue; private DateTime _lastKeepAliveSentUtc = DateTime.MinValue;
private Exception _lastConnectionException;
private WinoImapClient _dedicatedIdleClient; private WinoImapClient _dedicatedIdleClient;
private bool _disposedValue; private bool _disposedValue;
private bool _initialized; private bool _initialized;
@@ -114,7 +115,7 @@ public class ImapClientPool : IDisposable
var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
if (initialClient == null) 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; _clientStates[initialClient] = ImapClientState.Available;
@@ -192,6 +193,7 @@ public class ImapClientPool : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
_lastConnectionException = ex;
_logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed."); _logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed.");
MarkClientAsFailed(pooledClient); MarkClientAsFailed(pooledClient);
} }
@@ -215,12 +217,12 @@ public class ImapClientPool : IDisposable
} }
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) 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 throw cancellationToken.IsCancellationRequested
? new OperationCanceledException(cancellationToken) ? 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);
} }
/// <summary> /// <summary>
@@ -516,14 +518,17 @@ public class ImapClientPool : IDisposable
private async Task<WinoImapClient> CreateAndConnectClientAsync(CancellationToken cancellationToken) private async Task<WinoImapClient> CreateAndConnectClientAsync(CancellationToken cancellationToken)
{ {
var client = CreateNewClient(); var client = CreateNewClient();
_lastConnectionException = null;
try try
{ {
await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false); await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false);
_lastConnectionException = null;
return client; return client;
} }
catch (Exception ex) catch (Exception ex)
{ {
_lastConnectionException = ex;
_logger.Warning(ex, "Failed to create and connect IMAP client."); _logger.Warning(ex, "Failed to create and connect IMAP client.");
DisposeClient(client); DisposeClient(client);
return null; return null;
@@ -11,6 +11,7 @@ using Serilog;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models; using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.AutoDiscovery; using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Domain.Validation;
namespace Wino.Core.Services; namespace Wino.Core.Services;
@@ -450,6 +451,9 @@ public class AutoDiscoveryService : IAutoDiscoveryService
private async Task<bool> HasAnyDnsAddressRecordAsync(string host, CancellationToken cancellationToken) private async Task<bool> HasAnyDnsAddressRecordAsync(string host, CancellationToken cancellationToken)
{ {
if (MailAccountAddressValidator.IsImplicitlyResolvableHost(host))
return true;
var aRecords = await QueryDnsAsync(host, "A", cancellationToken).ConfigureAwait(false); var aRecords = await QueryDnsAsync(host, "A", cancellationToken).ConfigureAwait(false);
if (aRecords.Count > 0) if (aRecords.Count > 0)
return true; return true;
+1 -1
View File
@@ -123,7 +123,7 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient<Accoun
catch (ImapClientPoolException clientPoolException) catch (ImapClientPoolException clientPoolException)
{ {
_logger.Error(clientPoolException, "IMAP connectivity test failed"); _logger.Error(clientPoolException, "IMAP connectivity test failed");
return ImapConnectivityTestResults.Failure(clientPoolException); return ImapConnectivityTestResults.Failure(clientPoolException.InnerException ?? clientPoolException);
} }
catch (Exception exception) catch (Exception exception)
{ {
@@ -103,7 +103,10 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
return; return;
} }
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage)); Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step2Title,
WinoPage.ProviderSelectionPage,
ProviderSelectionNavigationContext.CreateForSettingsAddAccount()));
} }
public Task StartAddNewAccountAsync() => AddNewAccountAsync(); public Task StartAddNewAccountAsync() => AddNewAccountAsync();
@@ -10,7 +10,8 @@ public enum ImapCalDavSettingsPageMode
{ {
Create, Create,
Edit, Edit,
Wizard Wizard,
AddAccount
} }
public sealed class ImapCalDavSettingsNavigationContext public sealed class ImapCalDavSettingsNavigationContext
@@ -45,6 +46,14 @@ public sealed class ImapCalDavSettingsNavigationContext
AccountCreationDialogResult = accountCreationDialogResult AccountCreationDialogResult = accountCreationDialogResult
}; };
public static ImapCalDavSettingsNavigationContext CreateForAddAccountMode(
AccountCreationDialogResult accountCreationDialogResult)
=> new()
{
Mode = ImapCalDavSettingsPageMode.AddAccount,
AccountCreationDialogResult = accountCreationDialogResult
};
public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard; public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard;
} }
@@ -14,6 +14,7 @@ using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Domain.Validation;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
@@ -135,7 +136,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
[NotifyPropertyChangedFor(nameof(IsAdvancedSetupSelected))] [NotifyPropertyChangedFor(nameof(IsAdvancedSetupSelected))]
private int selectedSetupTabIndex; 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 IsEditMode => !IsCreateMode;
public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint); public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint);
public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0; public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0;
@@ -289,7 +290,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
_localOnlyInfoShown = false; _localOnlyInfoShown = false;
SelectedSetupTabIndex = 0; SelectedSetupTabIndex = 0;
if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard) if (_pageMode is ImapCalDavSettingsPageMode.Create or ImapCalDavSettingsPageMode.Wizard or ImapCalDavSettingsPageMode.AddAccount)
{ {
PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate; PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate;
ApplyCreateContextDefaults(context.AccountCreationDialogResult); ApplyCreateContextDefaults(context.AccountCreationDialogResult);
@@ -412,7 +413,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
? _editingAccountId ? _editingAccountId
: (Guid?)null; : (Guid?)null;
if (!await ValidateAccountUniquenessAsync(excludedAccountId).ConfigureAwait(false)) if (!await ValidateAccountUniquenessAsync(excludedAccountId))
return; return;
await ValidateImapConnectivityAsync(serverInformation); await ValidateImapConnectivityAsync(serverInformation);
@@ -434,6 +435,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
return; return;
} }
if (_pageMode == ImapCalDavSettingsPageMode.AddAccount)
{
CompleteAddAccountFlow(serverInformation);
return;
}
if (_pageMode == ImapCalDavSettingsPageMode.Create) if (_pageMode == ImapCalDavSettingsPageMode.Create)
{ {
CompleteCreateFlow(serverInformation); CompleteCreateFlow(serverInformation);
@@ -464,6 +471,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
} }
private void CompleteWizardFlow(CustomServerInformation serverInformation) private void CompleteWizardFlow(CustomServerInformation serverInformation)
=> ContinueAccountCreationFlow(serverInformation);
private void CompleteAddAccountFlow(CustomServerInformation serverInformation)
=> ContinueAccountCreationFlow(serverInformation);
private void ContinueAccountCreationFlow(CustomServerInformation serverInformation)
{ {
serverInformation.Id = Guid.NewGuid(); serverInformation.Id = Guid.NewGuid();
serverInformation.AccountId = Guid.Empty; 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) private async Task InitializeEditModeAsync(Guid accountId)
{ {
var account = await _accountService.GetAccountAsync(accountId); var account = await _accountService.GetAccountAsync(accountId);
@@ -618,6 +656,50 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SelectedOutgoingServerAuthenticationMethodIndex = 0; 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) private void ApplyServerInformation(CustomServerInformation serverInformation)
{ {
if (serverInformation == null) if (serverInformation == null)
@@ -809,7 +891,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task SaveEditFlowAsync(CustomServerInformation serverInformation) private async Task SaveEditFlowAsync(CustomServerInformation serverInformation)
{ {
var account = await _accountService.GetAccountAsync(_editingAccountId).ConfigureAwait(false); var account = await _accountService.GetAccountAsync(_editingAccountId);
if (account == null) if (account == null)
throw new InvalidOperationException(Translator.Exception_NullAssignedAccount); throw new InvalidOperationException(Translator.Exception_NullAssignedAccount);
@@ -823,8 +905,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
account.ServerInformation = serverInformation; account.ServerInformation = serverInformation;
account.AttentionReason = AccountAttentionReason.None; account.AttentionReason = AccountAttentionReason.None;
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false); await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation);
await _accountService.UpdateAccountAsync(account).ConfigureAwait(false); await _accountService.UpdateAccountAsync(account);
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
{ {
@@ -916,7 +998,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (string.IsNullOrWhiteSpace(EmailAddress)) if (string.IsNullOrWhiteSpace(EmailAddress))
throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailRequired); throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailRequired);
if (!EmailValidation.EmailValidator.Validate(EmailAddress.Trim())) if (!MailAccountAddressValidator.IsValid(EmailAddress))
throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailInvalid); throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailInvalid);
} }
@@ -21,6 +21,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
private readonly IDialogServiceBase _dialogService; private readonly IDialogServiceBase _dialogService;
private readonly IProviderService _providerService; private readonly IProviderService _providerService;
private readonly INewThemeService _themeService; private readonly INewThemeService _themeService;
private ProviderSelectionHostMode _hostMode = ProviderSelectionHostMode.Wizard;
public WelcomeWizardContext WizardContext { get; } public WelcomeWizardContext WizardContext { get; }
@@ -74,6 +75,16 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
{ {
base.OnNavigatedTo(mode, parameters); base.OnNavigatedTo(mode, parameters);
var navigationContext = parameters as ProviderSelectionNavigationContext
?? ProviderSelectionNavigationContext.CreateForWizard();
_hostMode = navigationContext.HostMode;
if (mode != NavigationMode.Back)
{
WizardContext.Reset();
}
Providers = _providerService.GetAvailableProviders(); Providers = _providerService.GetAvailableProviders();
AvailableColors = _themeService.GetAvailableAccountColors() AvailableColors = _themeService.GetAvailableAccountColors()
.Select(hex => new AppColorViewModel(hex)) .Select(hex => new AppColorViewModel(hex))
@@ -135,9 +146,11 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
if (WizardContext.IsGenericImap) if (WizardContext.IsGenericImap)
{ {
// Navigate to ImapCalDavSettingsPage in wizard mode var context = _hostMode == ProviderSelectionHostMode.SettingsAddAccount
var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode( ? ImapCalDavSettingsNavigationContext.CreateForAddAccountMode(
WizardContext.BuildAccountCreationDialogResult()); WizardContext.BuildAccountCreationDialogResult())
: ImapCalDavSettingsNavigationContext.CreateForWizardMode(
WizardContext.BuildAccountCreationDialogResult());
Messenger.Send(new BreadcrumbNavigationRequested( Messenger.Send(new BreadcrumbNavigationRequested(
Translator.ImapCalDavSettingsPage_TitleCreate, Translator.ImapCalDavSettingsPage_TitleCreate,
@@ -8,6 +8,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Validation;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
@@ -99,7 +100,7 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
{ {
CanProceed = !string.IsNullOrWhiteSpace(DisplayName) CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
&& !string.IsNullOrWhiteSpace(EmailAddress) && !string.IsNullOrWhiteSpace(EmailAddress)
&& EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty) && MailAccountAddressValidator.IsValid(EmailAddress)
&& !string.IsNullOrWhiteSpace(AppSpecificPassword); && !string.IsNullOrWhiteSpace(AppSpecificPassword);
} }
@@ -13,6 +13,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Updates; using Wino.Core.Domain.Models.Updates;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Messaging.UI; using Wino.Messaging.UI;
@@ -68,7 +69,8 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
{ {
Messenger.Send(new BreadcrumbNavigationRequested( Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step2Title, Translator.WelcomeWizard_Step2Title,
WinoPage.ProviderSelectionPage)); WinoPage.ProviderSelectionPage,
ProviderSelectionNavigationContext.CreateForWizard()));
} }
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))] [RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
@@ -8,6 +8,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Validation;
using Wino.Core.ViewModels.Data; using Wino.Core.ViewModels.Data;
using Wino.Helpers; using Wino.Helpers;
@@ -208,7 +209,7 @@ public sealed partial class NewAccountDialog : ContentDialog
&& !string.IsNullOrEmpty(AccountNameTextbox.Text) && !string.IsNullOrEmpty(AccountNameTextbox.Text)
&& (IsSpecialImapServerPartVisible ? (!string.IsNullOrEmpty(AppSpecificPassword.Password) && (IsSpecialImapServerPartVisible ? (!string.IsNullOrEmpty(AppSpecificPassword.Password)
&& !string.IsNullOrEmpty(DisplayNameTextBox.Text) && !string.IsNullOrEmpty(DisplayNameTextBox.Text)
&& EmailValidation.EmailValidator.Validate(SpecialImapAddress.Text)) : true); && MailAccountAddressValidator.IsValid(SpecialImapAddress.Text)) : true);
IsPrimaryButtonEnabled = shouldEnable; IsPrimaryButtonEnabled = shouldEnable;
} }