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>
382 lines
14 KiB
C#
382 lines
14 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Toolkit.Uwp.Notifications;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.Windows.AppLifecycle;
|
|
using Microsoft.Windows.AppNotifications;
|
|
using MimeKit.Cryptography;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.MailItem;
|
|
using Wino.Core.Domain.Models.Synchronization;
|
|
using Wino.Mail.Services;
|
|
using Wino.Mail.ViewModels;
|
|
using Wino.Mail.WinUI.Interfaces;
|
|
using Wino.Messaging.Client.Accounts;
|
|
using Wino.Messaging.Server;
|
|
using Wino.Services;
|
|
namespace Wino.Mail.WinUI;
|
|
|
|
public partial class App : WinoApplication, IRecipient<NewMailSynchronizationRequested>
|
|
{
|
|
private ISynchronizationManager? _synchronizationManager;
|
|
|
|
public App()
|
|
{
|
|
InitializeComponent();
|
|
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
|
|
|
|
RegisterRecipients();
|
|
}
|
|
|
|
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
|
{
|
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.AppNotification)
|
|
{
|
|
args = ((AppNotificationActivatedEventArgs)activationArgs.Data);
|
|
return true;
|
|
}
|
|
|
|
args = null!;
|
|
return false;
|
|
}
|
|
|
|
#region Dependency Injection
|
|
|
|
|
|
private void RegisterUWPServices(IServiceCollection services)
|
|
{
|
|
services.AddSingleton<INavigationService, NavigationService>();
|
|
services.AddSingleton<IMailDialogService, DialogService>();
|
|
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
|
|
services.AddTransient<IProviderService, ProviderService>();
|
|
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
|
}
|
|
|
|
private void RegisterViewModels(IServiceCollection services)
|
|
{
|
|
services.AddSingleton(typeof(AppShellViewModel));
|
|
|
|
services.AddTransient(typeof(MailListPageViewModel));
|
|
services.AddTransient(typeof(MailRenderingPageViewModel));
|
|
services.AddTransient(typeof(AccountManagementViewModel));
|
|
services.AddTransient(typeof(WelcomePageViewModel));
|
|
|
|
services.AddTransient(typeof(ComposePageViewModel));
|
|
services.AddTransient(typeof(IdlePageViewModel));
|
|
|
|
services.AddTransient(typeof(EditAccountDetailsPageViewModel));
|
|
services.AddTransient(typeof(AccountDetailsPageViewModel));
|
|
services.AddTransient(typeof(SignatureManagementPageViewModel));
|
|
services.AddTransient(typeof(MessageListPageViewModel));
|
|
services.AddTransient(typeof(ReadComposePanePageViewModel));
|
|
services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
|
|
services.AddTransient(typeof(LanguageTimePageViewModel));
|
|
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
|
services.AddTransient(typeof(AliasManagementPageViewModel));
|
|
services.AddTransient(typeof(ContactsPageViewModel));
|
|
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
|
}
|
|
|
|
#endregion
|
|
|
|
public override IServiceProvider ConfigureServices()
|
|
{
|
|
var services = new ServiceCollection();
|
|
|
|
services.RegisterViewModelService();
|
|
services.RegisterSharedServices();
|
|
services.RegisterCoreUWPServices();
|
|
services.RegisterCoreViewModels();
|
|
|
|
RegisterUWPServices(services);
|
|
RegisterViewModels(services);
|
|
|
|
return services.BuildServiceProvider();
|
|
}
|
|
|
|
private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask;
|
|
public bool IsAppRunning() => MainWindow != null;
|
|
|
|
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
|
{
|
|
base.OnLaunched(args);
|
|
|
|
AppNotificationManager notificationManager = AppNotificationManager.Default;
|
|
|
|
notificationManager.NotificationInvoked -= AppNotificationInvoked;
|
|
notificationManager.NotificationInvoked += AppNotificationInvoked;
|
|
notificationManager.Register();
|
|
|
|
// Initialize required services regardless of launch activation type.
|
|
// All activation scenarios require these services to be ready.
|
|
// Note: Theme service is initialized separately after window creation.
|
|
await InitializeServicesAsync();
|
|
|
|
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
|
|
|
// Check if launched from toast notification.
|
|
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
|
{
|
|
await HandleToastActivationAsync(toastArgs);
|
|
return;
|
|
}
|
|
|
|
// Check if launched by startup task.
|
|
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
|
|
|
// Create the window (needed for system tray icon even in startup task scenario).
|
|
CreateWindow(args);
|
|
|
|
// Initialize theme service after window creation.
|
|
// Theme service requires the window to exist to properly load and apply themes.
|
|
await NewThemeService.InitializeAsync();
|
|
LogActivation("Theme service initialized.");
|
|
|
|
// If startup task launch, keep window hidden (system tray only).
|
|
// Otherwise, activate the window normally.
|
|
if (isStartupTaskLaunch)
|
|
{
|
|
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
|
// Window is created but not activated. User can show it from system tray.
|
|
}
|
|
else
|
|
{
|
|
// Normal launch - show and activate the window.
|
|
MainWindow.Activate();
|
|
LogActivation("Window created and activated.");
|
|
}
|
|
}
|
|
|
|
private async void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
|
=> await HandleToastActivationAsync(args);
|
|
|
|
/// <summary>
|
|
/// Handles toast notification activation scenarios.
|
|
/// </summary>
|
|
private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs)
|
|
{
|
|
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
|
|
|
// Check if this is a navigation toast (user clicked the notification).
|
|
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) &&
|
|
Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId))
|
|
{
|
|
if (action == MailOperation.Navigate)
|
|
{
|
|
// User clicked notification - create window if needed and navigate.
|
|
await HandleToastNavigationAsync(mailItemUniqueId);
|
|
}
|
|
else
|
|
{
|
|
// User clicked action button (Mark as Read, Delete, etc.)
|
|
// Execute action without window and exit.
|
|
|
|
await HandleToastActionAsync(action, mailItemUniqueId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles toast notification click for navigation.
|
|
/// Creates window if not running, sets up navigation parameter.
|
|
/// </summary>
|
|
private async Task HandleToastNavigationAsync(Guid mailItemUniqueId)
|
|
{
|
|
var mailService = Services.GetRequiredService<IMailService>();
|
|
|
|
var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId).ConfigureAwait(false);
|
|
if (account == null) return;
|
|
|
|
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId).ConfigureAwait(false);
|
|
if (mailItem == null) return;
|
|
|
|
var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem);
|
|
|
|
// Store navigation parameter in LaunchProtocolService so AppShell can pick it up.
|
|
var launchProtocolService = Services.GetRequiredService<ILaunchProtocolService>();
|
|
launchProtocolService.LaunchParameter = message;
|
|
|
|
// Create window if not already created.
|
|
if (!IsAppRunning())
|
|
{
|
|
// Pass null for args since we're handling toast navigation
|
|
await CreateAndActivateWindow(null!);
|
|
}
|
|
else
|
|
{
|
|
// App is already running - send message and bring window to front.
|
|
WeakReferenceMessenger.Default.Send(message);
|
|
MainWindow.BringToFront();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles toast action button clicks (Mark as Read, Delete, etc.).
|
|
/// Executes the action without showing UI and exits the app.
|
|
/// </summary>
|
|
private async Task HandleToastActionAsync(MailOperation action, Guid mailItemUniqueId)
|
|
{
|
|
LogActivation($"Handling toast action: {action} for mail {mailItemUniqueId}");
|
|
|
|
var mailService = Services.GetRequiredService<IMailService>();
|
|
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId);
|
|
|
|
if (mailItem == null)
|
|
{
|
|
LogActivation("Mail item not found. Exiting.");
|
|
Application.Current.Exit();
|
|
return;
|
|
}
|
|
|
|
var package = new MailOperationPreperationRequest(action, mailItem);
|
|
|
|
// Check if app is already running (has a window).
|
|
if (IsAppRunning())
|
|
{
|
|
// App is running - use the simple delegator pattern.
|
|
// The synchronization will happen in the background.
|
|
LogActivation("App is running. Queueing request via delegator.");
|
|
|
|
var delegator = Services.GetRequiredService<IWinoRequestDelegator>();
|
|
await delegator.ExecuteAsync(package);
|
|
|
|
// Don't exit - app continues running.
|
|
LogActivation($"Toast action {action} queued successfully.");
|
|
}
|
|
else
|
|
{
|
|
// App is not running - we need to wait for sync before exiting.
|
|
LogActivation("App is not running. Executing synchronization and waiting for completion.");
|
|
|
|
if (_synchronizationManager == null)
|
|
{
|
|
LogActivation("Synchronization manager is not initialized. Exiting.");
|
|
Application.Current.Exit();
|
|
return;
|
|
}
|
|
|
|
var processor = Services.GetRequiredService<IWinoRequestProcessor>();
|
|
var notificationBuilder = Services.GetRequiredService<INotificationBuilder>();
|
|
|
|
// Prepare the requests for the action.
|
|
var requests = await processor.PrepareRequestsAsync(package);
|
|
|
|
if (requests != null && requests.Any())
|
|
{
|
|
// Group requests by account ID (usually just one account).
|
|
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
|
|
|
|
foreach (var accountGroup in accountIds)
|
|
{
|
|
var accountId = accountGroup.Key;
|
|
|
|
// Queue all requests for this account.
|
|
foreach (var request in accountGroup)
|
|
{
|
|
await _synchronizationManager.QueueRequestAsync(request, accountId, triggerSynchronization: false);
|
|
}
|
|
|
|
// Create synchronization options to execute the queued requests.
|
|
var syncOptions = new MailSynchronizationOptions()
|
|
{
|
|
AccountId = accountId,
|
|
Type = MailSynchronizationType.ExecuteRequests
|
|
};
|
|
|
|
LogActivation($"Executing synchronization for account {accountId}...");
|
|
|
|
// Wait for synchronization to complete before exiting.
|
|
var syncResult = await _synchronizationManager.SynchronizeMailAsync(syncOptions);
|
|
|
|
LogActivation($"Toast action {action} completed. Sync result: {syncResult.CompletedState}");
|
|
}
|
|
|
|
await notificationBuilder.UpdateTaskbarIconBadgeAsync();
|
|
}
|
|
|
|
LogActivation("Toast action handling complete. Exiting app.");
|
|
|
|
// Exit the app after synchronization is complete.
|
|
Application.Current.Exit();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the main window and activates it.
|
|
/// </summary>
|
|
private async Task CreateAndActivateWindow(LaunchActivatedEventArgs args)
|
|
{
|
|
CreateWindow(args);
|
|
|
|
// Initialize theme service after window is created.
|
|
await NewThemeService.InitializeAsync();
|
|
|
|
MainWindow.Activate();
|
|
LogActivation("Window created and activated.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the main window without activating it.
|
|
/// Used for both normal launch and startup task launch (tray only).
|
|
/// </summary>
|
|
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
|
{
|
|
LogActivation("Creating main window.");
|
|
|
|
MainWindow = new ShellWindow();
|
|
|
|
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
|
nativeAppService.GetCoreWindowHwnd = () => WinRT.Interop.WindowNative.GetWindowHandle(MainWindow);
|
|
|
|
if (MainWindow is not IWinoShellWindow shellWindow)
|
|
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
|
|
|
shellWindow.HandleAppActivation(args);
|
|
}
|
|
|
|
private void RegisterRecipients()
|
|
{
|
|
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(this);
|
|
}
|
|
|
|
public void Receive(NewMailSynchronizationRequested message)
|
|
{
|
|
_synchronizationManager?.SynchronizeMailAsync(message.Options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles activation redirected from another instance (single-instancing).
|
|
/// This is called when a second instance tries to launch and redirects to this existing instance.
|
|
/// </summary>
|
|
public void HandleRedirectedActivation(AppActivationArguments args)
|
|
{
|
|
// Dispatch to UI thread since this is called from Program.OnActivated
|
|
MainWindow?.DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
// Handle different activation kinds
|
|
if (args.Kind == ExtendedActivationKind.AppNotification)
|
|
{
|
|
// Handle toast notification activation
|
|
var toastArgs = (AppNotificationActivatedEventArgs)args.Data;
|
|
_ = HandleToastActivationAsync(toastArgs);
|
|
}
|
|
else
|
|
{
|
|
// For other activation types (Launch, Protocol, etc.), bring window to front
|
|
MainWindow?.BringToFront();
|
|
MainWindow?.Activate();
|
|
}
|
|
});
|
|
}
|
|
}
|