diff --git a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs b/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs index f33e775e..10b956f8 100644 --- a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs +++ b/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs @@ -1,21 +1,18 @@ -using System; -using Wino.Core.Domain.Models.AutoDiscovery; +using Wino.Core.Domain.Models.AutoDiscovery; namespace Wino.Core.Domain.Exceptions { public class ImapConnectionFailedPackage { - public ImapConnectionFailedPackage(Exception error, string protocolLog, AutoDiscoverySettings settings) + public ImapConnectionFailedPackage(string errorMessage, string protocolLog, AutoDiscoverySettings settings) { - Error = error; + ErrorMessage = errorMessage; ProtocolLog = protocolLog; Settings = settings; } public AutoDiscoverySettings Settings { get; } - public Exception Error { get; } + public string ErrorMessage { get; set; } public string ProtocolLog { get; } - - public string GetErrorMessage() => Error.InnerException == null ? Error.Message : Error.InnerException.Message; } } diff --git a/Wino.Core.Domain/Exceptions/ImapTestSSLCertificateException.cs b/Wino.Core.Domain/Exceptions/ImapTestSSLCertificateException.cs new file mode 100644 index 00000000..7b15c3cb --- /dev/null +++ b/Wino.Core.Domain/Exceptions/ImapTestSSLCertificateException.cs @@ -0,0 +1,17 @@ +namespace Wino.Core.Domain.Exceptions +{ + public class ImapTestSSLCertificateException : System.Exception + { + public ImapTestSSLCertificateException(string issuer, string expirationDateString, string validFromDateString) + { + Issuer = issuer; + ExpirationDateString = expirationDateString; + ValidFromDateString = validFromDateString; + } + + public string Issuer { get; set; } + public string ExpirationDateString { get; set; } + public string ValidFromDateString { get; set; } + + } +} diff --git a/Wino.Core.Domain/Extensions/ExceptionExtensions.cs b/Wino.Core.Domain/Extensions/ExceptionExtensions.cs new file mode 100644 index 00000000..b42a0206 --- /dev/null +++ b/Wino.Core.Domain/Extensions/ExceptionExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Domain.Extensions +{ + public static class ExceptionExtensions + { + public static IEnumerable GetInnerExceptions(this Exception ex) + { + if (ex == null) + { + throw new ArgumentNullException("ex"); + } + + var innerException = ex; + do + { + yield return innerException; + innerException = innerException.InnerException; + } + while (innerException != null); + } + } +} diff --git a/Wino.Core.Domain/Interfaces/IImapTestService.cs b/Wino.Core.Domain/Interfaces/IImapTestService.cs index 1288bc39..97d2c55e 100644 --- a/Wino.Core.Domain/Interfaces/IImapTestService.cs +++ b/Wino.Core.Domain/Interfaces/IImapTestService.cs @@ -5,6 +5,6 @@ namespace Wino.Core.Domain.Interfaces { public interface IImapTestService { - Task TestImapConnectionAsync(CustomServerInformation serverInformation); + Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake); } } diff --git a/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs b/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs new file mode 100644 index 00000000..715385e0 --- /dev/null +++ b/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Text.Json.Serialization; +using Wino.Core.Domain.Extensions; + +namespace Wino.Core.Domain.Models.Connectivity +{ + /// + /// Contains validation of the IMAP server connectivity during account setup. + /// + public class ImapConnectivityTestResults + { + [JsonConstructor] + protected ImapConnectivityTestResults() { } + + public bool IsSuccess { get; set; } + + public bool IsCertificateUIRequired { get; set; } + + public string FailedReason { get; set; } + public string FailureProtocolLog { get; set; } + + public static ImapConnectivityTestResults Success() => new ImapConnectivityTestResults() { IsSuccess = true }; + public static ImapConnectivityTestResults Failure(Exception ex, string failureProtocolLog) => new ImapConnectivityTestResults() + { + FailedReason = string.Join(Environment.NewLine, ex.GetInnerExceptions().Select(e => e.Message)), + FailureProtocolLog = failureProtocolLog + }; + + public static ImapConnectivityTestResults CertificateUIRequired(string issuer, + string expirationString, + string validFromString) + { + return new ImapConnectivityTestResults() + { + IsSuccess = false, + IsCertificateUIRequired = true, + CertificateIssuer = issuer, + CertificateExpirationDateString = expirationString, + CertificateValidFromDateString = validFromString + }; + } + + public string CertificateIssuer { get; set; } + public string CertificateValidFromDateString { get; set; } + public string CertificateExpirationDateString { get; set; } + } +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 0dce80f3..b190a618 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -34,6 +34,8 @@ "BasicIMAPSetupDialog_Title": "IMAP Account", "Buttons_AddAccount": "Add Account", "Buttons_AddNewAlias": "Add New Alias", + "Buttons_Allow": "Allow", + "Buttons_Deny": "Deny", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_ApplyTheme": "Apply Theme", "Buttons_Browse": "Browse", @@ -221,6 +223,14 @@ "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_ConnectionFailedTitle": "Connection Failed", "IMAPSetupDialog_ConnectionFailedMessage": "IMAP connection failed.", + "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "This server is requesting a SSL handshake to continue. Please confirm the certificate details below.", + "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Allow the handshake to continue setting up your account.", + "IMAPSetupDialog_CertificateIssuer": "Issuer", + "IMAPSetupDialog_CertificateSubject": "Subject", + "IMAPSetupDialog_CertificateValidFrom": "Valid from", + "IMAPSetupDialog_CertificateValidTo": "Valid to", + "IMAPSetupDialog_CertificateDenied": "User didn't authorize the handshake with the certificate.", + "IMAPSetupDialog_CertificateView": "View Certificate", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "InfoBarAction_Enable": "Enable", "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index e526cf8e..2a1dbcaa 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -193,6 +193,16 @@ namespace Wino.Core.Domain /// public static string Buttons_AddNewAlias => Resources.GetTranslatedString(@"Buttons_AddNewAlias"); + /// + /// Allow + /// + public static string Buttons_Allow => Resources.GetTranslatedString(@"Buttons_Allow"); + + /// + /// Deny + /// + public static string Buttons_Deny => Resources.GetTranslatedString(@"Buttons_Deny"); + /// /// Synchronize Aliases /// @@ -1128,6 +1138,46 @@ namespace Wino.Core.Domain /// public static string IMAPSetupDialog_ConnectionFailedMessage => Resources.GetTranslatedString(@"IMAPSetupDialog_ConnectionFailedMessage"); + /// + /// This server is requesting a SSL handshake to continue. Please confirm the certificate details below. + /// + public static string IMAPSetupDialog_CertificateAllowanceRequired_Row0 => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateAllowanceRequired_Row0"); + + /// + /// Allow the handshake to continue setting up your account. + /// + public static string IMAPSetupDialog_CertificateAllowanceRequired_Row1 => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateAllowanceRequired_Row1"); + + /// + /// Issuer + /// + public static string IMAPSetupDialog_CertificateIssuer => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateIssuer"); + + /// + /// Subject + /// + public static string IMAPSetupDialog_CertificateSubject => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateSubject"); + + /// + /// Valid from + /// + public static string IMAPSetupDialog_CertificateValidFrom => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateValidFrom"); + + /// + /// Valid to + /// + public static string IMAPSetupDialog_CertificateValidTo => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateValidTo"); + + /// + /// User didn't authorize the handshake with the certificate. + /// + public static string IMAPSetupDialog_CertificateDenied => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateDenied"); + + /// + /// View Certificate + /// + public static string IMAPSetupDialog_CertificateView => Resources.GetTranslatedString(@"IMAPSetupDialog_CertificateView"); + /// /// Image rendering is disabled for this message. /// diff --git a/Wino.Core.UWP/Services/WinoServerConnectionManager.cs b/Wino.Core.UWP/Services/WinoServerConnectionManager.cs index 4a7666c3..36153072 100644 --- a/Wino.Core.UWP/Services/WinoServerConnectionManager.cs +++ b/Wino.Core.UWP/Services/WinoServerConnectionManager.cs @@ -18,7 +18,6 @@ using Wino.Core.Integration.Json; using Wino.Messaging; using Wino.Messaging.Client.Connection; using Wino.Messaging.Enums; -using Wino.Messaging.Server; using Wino.Messaging.UI; namespace Wino.Core.UWP.Services diff --git a/Wino.Core/Integration/ImapClientPool.cs b/Wino.Core/Integration/ImapClientPool.cs index 1038a950..4d8125ff 100644 --- a/Wino.Core/Integration/ImapClientPool.cs +++ b/Wino.Core/Integration/ImapClientPool.cs @@ -2,6 +2,8 @@ using System.Collections.Concurrent; using System.IO; using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -9,6 +11,7 @@ using MailKit; using MailKit.Net.Imap; using MailKit.Net.Proxy; using MailKit.Security; +using MimeKit.Cryptography; using MoreLinq; using Serilog; using Wino.Core.Domain.Entities; @@ -42,6 +45,8 @@ namespace Wino.Core.Integration Name = "Wino Mail User", }; + public bool ThrowOnSSLHandshakeCallback { get; set; } + private readonly int MinimumPoolSize = 5; private readonly ConcurrentStack _clients = []; @@ -57,6 +62,8 @@ namespace Wino.Core.Integration // Set the maximum pool size to 5 or the custom value if it's greater. _semaphore = new(Math.Max(MinimumPoolSize, customServerInformation.MaxConcurrentClients)); + + CryptographyContext.Register(typeof(WindowsSecureMimeContext)); } private async Task EnsureConnectivityAsync(ImapClient client, bool isCreatedNew) @@ -107,6 +114,9 @@ namespace Wino.Core.Integration } catch (Exception ex) { + if (ex.InnerException is ImapTestSSLCertificateException imapTestSSLCertificateException) + throw imapTestSSLCertificateException; + throw new ImapClientPoolException(ex, GetProtocolLogContent()); } finally @@ -215,11 +225,27 @@ namespace Wino.Core.Integration { if (client.IsConnected) return; + client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback; + await client.ConnectAsync(_customServerInformation.IncomingServer, int.Parse(_customServerInformation.IncomingServerPort), GetSocketOptions(_customServerInformation.IncomingServerSocketOption)); } + bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // If there are no errors, then everything went smoothly. + if (sslPolicyErrors == SslPolicyErrors.None) return true; + + // Imap connectivity test will throw to alert the user here. + if (ThrowOnSSLHandshakeCallback) + { + throw new ImapTestSSLCertificateException(certificate.Issuer, certificate.GetExpirationDateString(), certificate.GetEffectiveDateString()); + } + + return true; + } + public async Task EnsureAuthenticatedAsync(ImapClient client) { if (client.IsAuthenticated) return; diff --git a/Wino.Core/Services/ImapTestService.cs b/Wino.Core/Services/ImapTestService.cs index ae7f92c3..eb2dbc89 100644 --- a/Wino.Core/Services/ImapTestService.cs +++ b/Wino.Core/Services/ImapTestService.cs @@ -34,11 +34,14 @@ namespace Wino.Core.Services _protocolLogStream = File.Create(logFile); } - public async Task TestImapConnectionAsync(CustomServerInformation serverInformation) + public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake) { EnsureProtocolLogFileExists(); - var clientPool = new ImapClientPool(serverInformation, _protocolLogStream); + var clientPool = new ImapClientPool(serverInformation, _protocolLogStream) + { + ThrowOnSSLHandshakeCallback = !allowSSLHandShake + }; using (clientPool) { diff --git a/Wino.Mail/Views/ImapSetup/AdvancedImapSetupPage.xaml b/Wino.Mail/Views/ImapSetup/AdvancedImapSetupPage.xaml index a1b164cc..26682b52 100644 --- a/Wino.Mail/Views/ImapSetup/AdvancedImapSetupPage.xaml +++ b/Wino.Mail/Views/ImapSetup/AdvancedImapSetupPage.xaml @@ -2,13 +2,13 @@ x:Class="Wino.Views.ImapSetup.AdvancedImapSetupPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:domain="using:Wino.Core.Domain" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - d:RequestedTheme="Dark" + xmlns:domain="using:Wino.Core.Domain" xmlns:helpers="using:Wino.Helpers" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" d:Background="Black" + d:RequestedTheme="Dark" mc:Ignorable="d"> @@ -18,31 +18,31 @@ - + + d:Text="Advanced IMAP / SMTP Configuration" + Style="{StaticResource TitleTextBlockStyle}" + Text="{x:Bind domain:Translator.IMAPSetupDialog_Title}" /> + PlaceholderText="{x:Bind domain:Translator.IMAPSetupDialog_MailAddressPlaceholder}" /> + PlaceholderText="{x:Bind domain:Translator.IMAPSetupDialog_DisplayNamePlaceholder}" /> @@ -55,34 +55,34 @@ + PlaceholderText="eg. imap.gmail.com" + TextChanged="IncomingServerChanged" /> + Grid.Column="1" + d:Header="Port" + Header="{x:Bind domain:Translator.IMAPSetupDialog_IncomingMailServerPort}" + /> + TextChanged="IncomingUsernameChanged" /> + PasswordChanged="IncomingPasswordChanged" /> @@ -94,35 +94,35 @@ + Text="{x:Bind domain:Translator.ImapAdvancedSetupDialog_ConnectionSecurity}" /> + SelectedIndex="0" /> + Text="{x:Bind domain:Translator.ImapAdvancedSetupDialog_AuthenticationMethod}" /> + SelectedIndex="0" /> - + @@ -133,18 +133,18 @@ + PlaceholderText="eg. smtp.gmail.com" + TextChanged="OutgoingServerChanged" /> + /> @@ -152,13 +152,13 @@ + Header="{x:Bind domain:Translator.IMAPSetupDialog_OutgoingMailServerUsername}" + IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(UseSameCredentialsForSending), Mode=OneWay}" /> + Header="{x:Bind domain:Translator.IMAPSetupDialog_OutgoingMailServerPassword}" + IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(UseSameCredentialsForSending), Mode=OneWay}" /> @@ -169,42 +169,42 @@ - + + SelectedIndex="0" /> - + + SelectedIndex="0" /> - + - + - + + Header="Port" /> @@ -214,10 +214,10 @@ @@ -225,18 +225,18 @@