beb3bf9d1d
* feat: add S/MIME certificate management - Introduced `ISmimeCertificateService` interface for managing S/MIME certificates. - Implemented `SmimeCertificateService` class to handle certificate operations. - Updated `WinoPage` enum to include `SignatureAndEncryptionPage`. - Added resource entries in `resources.json` for S/MIME related messages. - Created `SignatureAndEncryptionPage` view and logic for user interaction. - Modified configuration files to integrate the new service and page. - Updated project files to include necessary dependencies for certificate management. * refactor(SmimeCertificateService): ♻️ Use constant for certificate name Refactored the `SmimeCertificateService` to replace the hardcoded string "Wino Mail Certificate" with a constant `CertificateFriendlyName`. This change enhances code maintainability by centralizing the definition of the certificate's friendly name. • Introduced a constant for the certificate's friendly name. • Updated the certificate retrieval and import logic to use the new constant. * feat(alias): ✨ Add S/Mime certificate selection for every alias Added new properties and methods in `MailAccountAlias` to manage signing and encryption certificates, including their thumbprints. This enhancement allows for better handling of S/Mime certificates within the application. • Introduced new properties for signing and encryption certificates. • Updated `resources.json` with new translations for S/Mime certificates. • Enhanced `AliasManagementPageViewModel` to include a dependency on the S/Mime certificate service and updated alias loading methods. • Modified `AliasManagementPage.xaml` to include ComboBox controls for selecting certificates. • Implemented methods in `AliasManagementPage.xaml.cs` to handle certificate selection from dropdowns. This change improves the user experience by allowing users to select and manage their S/Mime certificates directly within the alias management interface. * feat(mail): ✨ Add S/MIME support and file picker updates Enhanced the `MailRenderModel` class by adding a new property `IsSmimeSigned` to indicate if an email is S/MIME signed. The constructor has been updated to accept `MailRenderingOptions`. Updated the file selection logic in `DialogServiceBase` to replace the `FolderPicker` with a `FileSavePicker`, streamlining the process of saving files. Removed unnecessary commented code and added logic to handle file extensions. In `MailRenderingPageViewModel`, a new property `IsSmimeSigned` reflects the S/MIME status of the current render model, along with a new method `ShowSmimeCertificateInfoAsync` to display S/MIME certificate details. Added a `HyperlinkButton` in `MailRenderingPage.xaml` to indicate S/MIME status, which is only visible for signed emails, providing a tooltip and command for more information. In `MimeFileService`, implemented logic to detect S/MIME signatures in messages and exclude S/MIME signature parts from attachments. * refactor(viewmodel): ♻️ Replace dialog service messages Refactored the `SignatureAndEncryptionPageViewModel.cs` to replace calls to `_dialogService.ShowMessageAsync` with `_dialogService.InfoBarMessage`. This change improves the handling of success messages during certificate import and removal processes. * feat(mail): ✨ Add S/MIME encryption indicator Implemented support for S/MIME email handling in the MailRenderingPageViewModel. This includes the addition of a new property to check if an email is encrypted and updates to methods for displaying S/MIME certificate information. A new column was added in the MailRenderingPage.xaml to indicate if an email is encrypted, along with updated tooltips and commands. The MimeFileService was also modified to detect S/MIME encryption and to exclude S/MIME signature certificates during attachment processing. * fix: Added missing property * feat: Added S/Mime decryption and signing verification and improvements * i18n(resources): 🌐 Add S/MIME translation strings Added new translation strings for S/MIME functionalities in `resources.json`, including messages for signatures and certificates in both English and Italian. The code has been updated to utilize these new translation strings, enhancing the application's internationalization. Updated `MailRenderingPageViewModel.cs` to use the new translation strings for signature and certificate messages, improving code readability and consistency with translations. Additionally, the tooltips for S/MIME signing and encryption buttons in `MailRenderingPage.xaml` have been updated to use the new translation strings, enhancing the user experience for Italian-speaking users. * fix: Extract body from MultipartSigned message * feat(smime): ✨ Enhance S/MIME certificate handling Updated the `SmimeCertificateService` to improve the loading of PKCS12 certificate collections by adding `X509KeyStorageFlags.DefaultKeySet` and `X509KeyStorageFlags.Exportable` for better key management. In `ComposePageViewModel`, imported necessary namespaces for S/MIME certificate handling and added a new dependency for `ISmimeCertificateService`. Implemented logic in `OpenAttachmentAsync` to load alias certificates and manage message signing and encryption based on user-selected certificates. This change enhances the security and flexibility of email handling within the application. * feat: Replaced Smime encryption certificate combobox with checkbox Cert selection is useless for encryption * feat: Added S/Mime togglebuttons when composing an email * i18n(translations): 🌐 Add new composer translations Added new translation strings for composer features, including themes, text formatting, and S/MIME signing and encryption options. Updated button labels to utilize these new strings, enhancing the application's internationalization. Additionally, removed an obsolete string related to S/MIME certificate file information. * Example for relay command and fix settings pages runtime error * refactor(viewmodel): ♻️ Update certificate import/export commands Refactored the certificate import and export commands in the `SignatureAndEncryptionPageViewModel`. Changed methods from `async void` to `async Task` for better error handling and tracking of asynchronous operations. Added `[RelayCommand]` attributes to improve adherence to the MVVM pattern. Updated the XAML file to bind buttons directly to the new command methods, removing the need for event handlers. This enhances separation of concerns and simplifies the code. Removed obsolete event handlers from the code-behind file, streamlining the implementation. * fix: export folderPath parameter contains file name * fix: QRESYNC initial modseq should be 1 (#734) * Fix typo in reorder accounts dialog (#754) * fix: Missing commas in translations files * fix: merge issues * Fix mege conflicts. * Some more conflict fixes. * Fixing context. * Fixing saving file with suggested file name. --------- Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com> Co-authored-by: Konstantin Shkel <null+github@pcho.la> Co-authored-by: Cas Cornelissen <cas.cornelissen@onefinity.io> Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
347 lines
12 KiB
C#
347 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Serilog;
|
|
using Windows.Storage;
|
|
using Windows.Storage.AccessCache;
|
|
using Windows.Storage.Pickers;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Accounts;
|
|
using Wino.Core.Domain.Models.Common;
|
|
using Wino.Core.Domain.Models.Printing;
|
|
using Wino.Dialogs;
|
|
using Wino.Mail.WinUI.Dialogs;
|
|
using Wino.Mail.WinUI.Extensions;
|
|
using Wino.Messaging.Client.Shell;
|
|
using WinRT.Interop;
|
|
|
|
namespace Wino.Mail.WinUI.Services;
|
|
|
|
public class DialogServiceBase : IDialogServiceBase
|
|
{
|
|
private SemaphoreSlim _presentationSemaphore = new SemaphoreSlim(1);
|
|
|
|
protected INewThemeService ThemeService { get; }
|
|
protected IConfigurationService ConfigurationService { get; }
|
|
|
|
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
|
|
|
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
|
{
|
|
ThemeService = themeService;
|
|
ConfigurationService = configurationService;
|
|
ApplicationResourceManager = applicationResourceManager;
|
|
}
|
|
|
|
protected XamlRoot GetXamlRoot()
|
|
{
|
|
return WinoApplication.MainWindow?.Content?.XamlRoot;
|
|
}
|
|
|
|
public async Task<string> PickFilePathAsync(string saveFileName)
|
|
{
|
|
var picker = new FolderPicker()
|
|
{
|
|
SuggestedStartLocation = PickerLocationId.Desktop,
|
|
};
|
|
|
|
picker.FileTypeFilter.Add("*");
|
|
|
|
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
|
InitializeWithWindow.Initialize(picker, windowHandle);
|
|
|
|
var folder = await picker.PickSingleFolderAsync();
|
|
if (folder == null) return string.Empty;
|
|
|
|
StorageApplicationPermissions.FutureAccessList.Add(folder);
|
|
|
|
return $"{Path.Combine(folder.Path, saveFileName)}";
|
|
|
|
//var picker = new FileSavePicker
|
|
//{
|
|
// SuggestedStartLocation = PickerLocationId.Desktop,
|
|
// SuggestedFileName = saveFileName
|
|
//};
|
|
|
|
//picker.FileTypeChoices.Add(Translator.FilteringOption_All, [".*"]);
|
|
|
|
//var file = await picker.PickSaveFileAsync();
|
|
//if (file == null) return string.Empty;
|
|
|
|
//StorageApplicationPermissions.FutureAccessList.Add(file);
|
|
|
|
//return file.Path;
|
|
}
|
|
|
|
public async Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters)
|
|
{
|
|
var returnList = new List<SharedFile>();
|
|
var picker = new FileOpenPicker
|
|
{
|
|
ViewMode = PickerViewMode.Thumbnail,
|
|
SuggestedStartLocation = PickerLocationId.Desktop
|
|
};
|
|
|
|
foreach (var filter in typeFilters)
|
|
{
|
|
picker.FileTypeFilter.Add(filter.ToString());
|
|
}
|
|
|
|
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
|
InitializeWithWindow.Initialize(picker, windowHandle);
|
|
|
|
var files = await picker.PickMultipleFilesAsync();
|
|
if (files == null) return returnList;
|
|
|
|
foreach (var file in files)
|
|
{
|
|
StorageApplicationPermissions.FutureAccessList.Add(file);
|
|
|
|
var sharedFile = await file.ToSharedFileAsync();
|
|
returnList.Add(sharedFile);
|
|
}
|
|
|
|
return returnList;
|
|
}
|
|
|
|
private async Task<StorageFile> PickFileAsync(params object[] typeFilters)
|
|
{
|
|
var picker = new FileOpenPicker
|
|
{
|
|
ViewMode = PickerViewMode.Thumbnail
|
|
};
|
|
|
|
foreach (var filter in typeFilters)
|
|
{
|
|
picker.FileTypeFilter.Add(filter.ToString());
|
|
}
|
|
|
|
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
|
InitializeWithWindow.Initialize(picker, windowHandle);
|
|
|
|
var file = await picker.PickSingleFileAsync();
|
|
|
|
if (file == null) return null;
|
|
|
|
StorageApplicationPermissions.FutureAccessList.Add(file);
|
|
|
|
return file;
|
|
}
|
|
|
|
public virtual IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult)
|
|
{
|
|
return new AccountCreationDialog
|
|
{
|
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
|
XamlRoot = GetXamlRoot()
|
|
};
|
|
}
|
|
|
|
public async Task<byte[]> PickWindowsFileContentAsync(params object[] typeFilters)
|
|
{
|
|
var file = await PickFileAsync(typeFilters);
|
|
|
|
if (file == null) return [];
|
|
|
|
return await file.ToByteArrayAsync();
|
|
}
|
|
|
|
public Task ShowMessageAsync(string message, string title, WinoCustomMessageDialogIcon icon = WinoCustomMessageDialogIcon.Information)
|
|
=> ShowWinoCustomMessageDialogAsync(title, message, Translator.Buttons_Close, icon);
|
|
|
|
public Task<bool> ShowConfirmationDialogAsync(string question, string title, string confirmationButtonTitle)
|
|
=> ShowWinoCustomMessageDialogAsync(title, question, confirmationButtonTitle, WinoCustomMessageDialogIcon.Question, Translator.Buttons_Cancel, string.Empty);
|
|
|
|
public async Task<bool> ShowWinoCustomMessageDialogAsync(string title,
|
|
string description,
|
|
string approveButtonText,
|
|
WinoCustomMessageDialogIcon? icon,
|
|
string cancelButtonText = "",
|
|
string dontAskAgainConfigurationKey = "")
|
|
|
|
{
|
|
// This config key has been marked as don't ask again already.
|
|
// Return immidiate result without presenting the dialog.
|
|
|
|
bool isDontAskEnabled = !string.IsNullOrEmpty(dontAskAgainConfigurationKey);
|
|
|
|
if (isDontAskEnabled && ConfigurationService.Get(dontAskAgainConfigurationKey, false)) return false;
|
|
|
|
var informationContainer = new CustomMessageDialogInformationContainer(title, description, icon.Value, isDontAskEnabled);
|
|
|
|
var dialog = new ContentDialog
|
|
{
|
|
Style = ApplicationResourceManager.GetResource<Style>("WinoDialogStyle"),
|
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
|
DefaultButton = ContentDialogButton.Primary,
|
|
PrimaryButtonText = approveButtonText,
|
|
ContentTemplate = ApplicationResourceManager.GetResource<DataTemplate>("CustomWinoContentDialogContentTemplate"),
|
|
Content = informationContainer
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(cancelButtonText))
|
|
{
|
|
dialog.SecondaryButtonText = cancelButtonText;
|
|
}
|
|
|
|
var dialogResult = await HandleDialogPresentationAsync(dialog);
|
|
|
|
// Mark this key to not ask again if user checked the checkbox.
|
|
if (informationContainer.IsDontAskChecked)
|
|
{
|
|
ConfigurationService.Set(dontAskAgainConfigurationKey, true);
|
|
}
|
|
|
|
return dialogResult == ContentDialogResult.Primary;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for PopupRoot to be available before presenting the dialog and returns the result after presentation.
|
|
/// </summary>
|
|
/// <param name="dialog">Dialog to present and wait for closing.</param>
|
|
/// <returns>Dialog result from WinRT.</returns>
|
|
public async Task<ContentDialogResult> HandleDialogPresentationAsync(ContentDialog dialog)
|
|
{
|
|
await _presentationSemaphore.WaitAsync();
|
|
|
|
try
|
|
{
|
|
dialog.XamlRoot = GetXamlRoot();
|
|
|
|
return await dialog.ShowAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, $"Handling dialog service failed. Dialog was {dialog.GetType().Name}");
|
|
}
|
|
finally
|
|
{
|
|
_presentationSemaphore.Release();
|
|
}
|
|
|
|
return ContentDialogResult.None;
|
|
}
|
|
|
|
|
|
public void InfoBarMessage(string title, string message, InfoBarMessageType messageType)
|
|
=> WeakReferenceMessenger.Default.Send(new InfoBarMessageRequested(messageType, title, message));
|
|
|
|
public void InfoBarMessage(string title, string message, InfoBarMessageType messageType, string actionButtonText, Action action)
|
|
=> WeakReferenceMessenger.Default.Send(new InfoBarMessageRequested(messageType, title, message, actionButtonText, action));
|
|
|
|
public void ShowNotSupportedMessage()
|
|
=> InfoBarMessage(Translator.Info_UnsupportedFunctionalityTitle,
|
|
Translator.Info_UnsupportedFunctionalityDescription,
|
|
InfoBarMessageType.Error);
|
|
|
|
public async Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription, string primaryButtonText)
|
|
{
|
|
var inputDialog = new TextInputDialog()
|
|
{
|
|
CurrentInput = currentInput,
|
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
|
Title = dialogTitle
|
|
};
|
|
|
|
inputDialog.SetDescription(dialogDescription);
|
|
inputDialog.SetPrimaryButtonText(primaryButtonText);
|
|
|
|
await HandleDialogPresentationAsync(inputDialog);
|
|
|
|
if (inputDialog.HasInput.GetValueOrDefault() && !currentInput.Equals(inputDialog.CurrentInput))
|
|
return inputDialog.CurrentInput;
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
public async Task<string> PickWindowsFolderAsync()
|
|
{
|
|
var picker = new FolderPicker
|
|
{
|
|
SuggestedStartLocation = PickerLocationId.DocumentsLibrary
|
|
};
|
|
|
|
picker.FileTypeFilter.Add("*");
|
|
|
|
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
|
InitializeWithWindow.Initialize(picker, windowHandle);
|
|
|
|
var pickedFolder = await picker.PickSingleFolderAsync();
|
|
|
|
if (pickedFolder != null)
|
|
{
|
|
Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("FolderPickerToken", pickedFolder);
|
|
|
|
return pickedFolder.Path;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
public async Task<bool> ShowCustomThemeBuilderDialogAsync()
|
|
{
|
|
var themeBuilderDialog = new CustomThemeBuilderDialog()
|
|
{
|
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
|
};
|
|
|
|
var dialogResult = await HandleDialogPresentationAsync(themeBuilderDialog);
|
|
|
|
return dialogResult == ContentDialogResult.Primary;
|
|
}
|
|
|
|
public async Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders)
|
|
{
|
|
var dialog = new NewAccountDialog
|
|
{
|
|
Providers = availableProviders,
|
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
|
};
|
|
|
|
await HandleDialogPresentationAsync(dialog);
|
|
|
|
return dialog.Result;
|
|
}
|
|
|
|
public async Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null)
|
|
{
|
|
try
|
|
{
|
|
// Create the print dialog
|
|
var dialog = initialSettings != null
|
|
? new PrintDialog(initialSettings)
|
|
: new PrintDialog();
|
|
|
|
// Set the XamlRoot for proper display
|
|
dialog.XamlRoot = GetXamlRoot();
|
|
|
|
// Get available printers asynchronously when the dialog is loaded
|
|
dialog.Loaded += async (sender, e) =>
|
|
{
|
|
await dialog.LoadAvailablePrintersAsync();
|
|
};
|
|
|
|
// Show the dialog
|
|
var result = await HandleDialogPresentationAsync(dialog);
|
|
|
|
// Return the settings if user clicked Print, otherwise null
|
|
return result == ContentDialogResult.Primary
|
|
? dialog.PrintSettings
|
|
: null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log the exception if logging is available
|
|
Log.Error(ex, "Error showing print dialog");
|
|
return null;
|
|
}
|
|
}
|
|
}
|