From beb3bf9d1d5c0576ae1c693c18522b0e111cf8fc Mon Sep 17 00:00:00 2001 From: Maicol Battistini Date: Sun, 23 Nov 2025 20:56:57 +0100 Subject: [PATCH] feat: S/MIME signing and encryption (#693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Konstantin Shkel Co-authored-by: Cas Cornelissen Co-authored-by: Burak Kaan Köse --- .../Entities/Mail/MailAccountAlias.cs | 11 + Wino.Core.Domain/Enums/WinoPage.cs | 3 +- .../Interfaces/ISmimeCertificateService.cs | 11 + Wino.Core.Domain/Models/Common/SharedFile.cs | 1 + .../Models/MailItem/HtmlPreviewVisitor.cs | 52 +++- .../Models/Reader/MailRenderModel.cs | 6 + .../Translations/en_US/resources.json | 55 +++- .../SettingOptionsPageViewModel.cs | 1 + .../ImapSync/QResyncSynchronizer.cs | 4 + .../AliasManagementPageViewModel.cs | 39 ++- Wino.Mail.ViewModels/ComposePageViewModel.cs | 104 +++++++- .../MailRenderingPageViewModel.cs | 105 ++++++-- .../SignatureAndEncryptionPageViewModel.cs | 238 ++++++++++++++++++ .../Wino.Mail.ViewModels.csproj | 1 + Wino.Mail.WinUI/App.xaml.cs | 6 +- Wino.Mail.WinUI/CoreUWPContainerSetup.cs | 3 + Wino.Mail.WinUI/Services/DialogServiceBase.cs | 7 +- Wino.Mail.WinUI/Services/NavigationService.cs | 1 + .../Services/SettingsBuilderService.cs | 1 + .../Services/SmimeCertificateService.cs | 62 +++++ .../SignatureAndEncryptionPageAbstract.cs | 8 + Wino.Mail.WinUI/Views/ComposePage.xaml | 70 +++++- Wino.Mail.WinUI/Views/MailRenderingPage.xaml | 44 ++++ .../Views/Settings/AliasManagementPage.xaml | 36 ++- .../Settings/AliasManagementPage.xaml.cs | 27 ++ .../Settings/SignatureAndEncryptionPage.xaml | 184 ++++++++++++++ .../SignatureAndEncryptionPage.xaml.cs | 34 +++ Wino.Services/MimeFileService.cs | 17 +- 28 files changed, 1079 insertions(+), 52 deletions(-) create mode 100644 Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs create mode 100644 Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs create mode 100644 Wino.Mail.WinUI/Services/SmimeCertificateService.cs create mode 100644 Wino.Mail.WinUI/Views/Abstract/SignatureAndEncryptionPageAbstract.cs create mode 100644 Wino.Mail.WinUI/Views/Settings/SignatureAndEncryptionPage.xaml create mode 100644 Wino.Mail.WinUI/Views/Settings/SignatureAndEncryptionPage.xaml.cs diff --git a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs index f13dd5ef..b23aaf4e 100644 --- a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs +++ b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.ObjectModel; +using System.Security.Cryptography.X509Certificates; using SQLite; namespace Wino.Core.Domain.Entities.Mail; @@ -59,4 +61,13 @@ public class MailAccountAlias : RemoteAccountAlias /// Root aliases can't be deleted. /// public bool CanDelete => !IsRootAlias; + + public string SelectedSigningCertificateThumbprint { get; set; } + public bool IsSmimeEncryptionEnabled { get; set; } + + [Ignore] + public X509Certificate2 SelectedSigningCertificate { get; set; } + + [Ignore] + public ObservableCollection Certificates { get; set; } = []; } diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index 3bef698d..0b563fa5 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -30,5 +30,6 @@ public enum WinoPage KeyboardShortcutsPage, CalendarPage, CalendarSettingsPage, - EventDetailsPage + EventDetailsPage, + SignatureAndEncryptionPage } diff --git a/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs new file mode 100644 index 00000000..9b9fee6a --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Wino.Core.Domain.Interfaces; + +public interface ISmimeCertificateService +{ + public IEnumerable GetCertificates(StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser, string emailAddress = null); + public void ImportCertificate(string fileExtension, byte[] rawData, string password = null, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser); + public void RemoveCertificate(string thumbprint, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser); +} diff --git a/Wino.Core.Domain/Models/Common/SharedFile.cs b/Wino.Core.Domain/Models/Common/SharedFile.cs index c5b28f6a..fe1c3640 100644 --- a/Wino.Core.Domain/Models/Common/SharedFile.cs +++ b/Wino.Core.Domain/Models/Common/SharedFile.cs @@ -10,4 +10,5 @@ namespace Wino.Core.Domain.Models.Common; public record SharedFile(string FullFilePath, byte[] Data) { public string FileName => Path.GetFileName(FullFilePath); + public string FileExtension => Path.GetExtension(FullFilePath)?.ToLowerInvariant(); } diff --git a/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs b/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs index deb35a54..c62c9a6f 100644 --- a/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs +++ b/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using MimeKit; +using MimeKit.Cryptography; using MimeKit.Text; using MimeKit.Tnef; @@ -18,6 +19,7 @@ public class HtmlPreviewVisitor : MimeVisitor readonly string tempDir; public string Body { get; set; } + public Dictionary Signatures = []; /// /// Creates a new HtmlPreviewVisitor. @@ -65,6 +67,12 @@ public class HtmlPreviewVisitor : MimeVisitor stack.RemoveAt(stack.Count - 1); } + protected override void VisitMultipartSigned(MultipartSigned signed) + { + VerifySignatures(signed.Verify()); + VisitMultipart(signed); + } + // look up the image based on the img src url within our multipart/related stack bool TryGetImage(string url, out MimePart image) { @@ -247,8 +255,46 @@ public class HtmlPreviewVisitor : MimeVisitor protected override void VisitMimePart(MimePart entity) { - // realistically, if we've gotten this far, then we can treat this as an attachment - // even if the IsAttachment property is false. - attachments.Add(entity); + if (entity is ApplicationPkcs7Mime { SecureMimeType: SecureMimeType.EnvelopedData } encrypted) + { + encrypted.Decrypt().Accept(this); + } + else if (entity is ApplicationPkcs7Mime { SecureMimeType: SecureMimeType.SignedData } signed) + { + MimeEntity extracted; + + VerifySignatures(signed.Verify(out extracted)); + + extracted.Accept(this); + } + else + { + // realistically, if we've gotten this far, then we can treat this as an attachment + // even if the IsAttachment property is false. + attachments.Add(entity); + } + } + + private void VerifySignatures(DigitalSignatureCollection signatures) + { + foreach (var signature in signatures) + { + try + { + bool valid = signature.Verify(); + Signatures.Add(signature, valid); + + // If valid is true, then it signifies that the signed content has not + // been modified since this particular signer signed the content. + // + // However, if it is false, then it indicates that the signed content + // has been modified. + } + catch (DigitalSignatureVerifyException) + { + // There was an error verifying the signature. + Signatures.Add(signature, false); + } + } } } diff --git a/Wino.Core.Domain/Models/Reader/MailRenderModel.cs b/Wino.Core.Domain/Models/Reader/MailRenderModel.cs index 5ca16b78..1bcae631 100644 --- a/Wino.Core.Domain/Models/Reader/MailRenderModel.cs +++ b/Wino.Core.Domain/Models/Reader/MailRenderModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using MimeKit; +using MimeKit.Cryptography; namespace Wino.Core.Domain.Models.Reader; @@ -15,6 +16,11 @@ public class MailRenderModel public UnsubscribeInfo UnsubscribeInfo { get; set; } + public Dictionary Signatures = []; + + // Indicates if the mail is S/MIME encrypted + public bool IsSmimeEncrypted { get; set; } + public MailRenderModel(string renderHtml, MailRenderingOptions mailRenderingOptions = null) { RenderHtml = renderHtml; diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 1e7ca6d0..8c6a1e67 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -727,6 +727,59 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Certificates imported successfully.", + "Smime_ImportCertificates_Error": "Error importing certificates: {0}", + "Smime_RemoveCertificates_Confirm": "Do you really want to remove the certificates {0}?", + "Smime_RemoveCertificates_Success": "Certificates removed.", + "Smime_ExportCertificates_Success": "Certificates exported.", + "Smime_ExportCertificates_Error": "Error exporting certificates.", + "Smime_CertificateDetails": "Subject: {0}\nIssuer: {1}\nValid from: {2}\nValid to: {3}\nThumbprint: {4}", + "Smime_CertificatePassword_Title": "Certificate password required", + "Smime_CertificatePassword_Placeholder": "Certificate password for {0} (optional)", + "Smime_Confirm_Title": "Confirm", + "Buttons_OK": "OK", + "SettingsSignatureAndEncryption_Title": "Signature and Encryption", + "SettingsSignatureAndEncryption_Description": "Manage S/MIME certificates for signing and encrypting emails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "My certificates", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Personal certificates for signing and encryption", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Recipient certificates", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Recipient certificates for decryption", + "SettingsSignatureAndEncryption_NameColumn": "Name", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expires on", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Thumbprint", + "Buttons_Remove": "Remove", + "Buttons_Export": "Export", + "Buttons_Import": "Import", + "SettingsSignatureAndEncryption_SigningCertificate": "S/Mime Signing Certificate", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/Mime Encryption", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "None", + "SmimeSignaturesInMessage": "Signatures in this message:", + "SmimeSignatureEntry": "• {0} {1} ({2}, valid through {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME Signing Certificate Info", + "SmimeCertificateInfoTitle": "S/MIME Certificate Info", + "SmimeNoCertificateFileFound": "No certificate file found", + "SmimeSaveCertificate": "Save certificate...", + "SmimeCertificate": "S/MIME Certificate", + "SmimeCertificateSavedTo": "Certificate saved to {0}", + "SmimeSignedTooltip": "This message is signed with an S/Mime certificate. Click for more details", + "SmimeEncryptedTooltip": "This message is encrypted with an S/Mime certificate.", + "SmimeCertificateFileInfo": "File: {0}\nType: {1}\nSize: {2:N0} bytes", + "Composer_LightTheme": "Light Theme", + "Composer_DarkTheme": "Dark Theme", + "Composer_Outdent": "Outdent", + "Composer_Indent": "Indent", + "Composer_BulletList": "Bullet List", + "Composer_OrderedList": "Ordered List", + "Composer_Stroke": "Stroke", + "Composer_Bold": "Bold", + "Composer_Italic": "Italic", + "Composer_Underline": "Underline", + "Composer_CcBcc": "Cc & Bcc", + "Composer_EnableSmimeSignature": "Enable/disable S/MIME signature", + "Composer_EnableSmimeEncryption": "Enable/disable S/MIME encryption", + "Composer_CertificateExpires": "Expires on: ", + "Composer_SmimeSignature": "S/MIME Signature", + "Composer_SmimeEncryption": "S/MIME Encryption", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", "ContactsPage_Title": "Contacts", @@ -762,5 +815,3 @@ "ContactsPage_EmptyState": "No contacts to display", "ContactsPage_AddFirstContact": "Add your first contact" } - - diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index 8745056f..c653dc67 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -38,6 +38,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel WinoPage.LanguageTimePage => Translator.SettingsLanguageTime_Title, WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, WinoPage.CalendarSettingsPage => Translator.SettingsCalendarSettings_Title, + WinoPage.SignatureAndEncryptionPage => Translator.SettingsSignatureAndEncryption_Title, WinoPage.KeyboardShortcutsPage => "Keyboard Shortcuts", _ => throw new NotImplementedException() }; diff --git a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs index 01cb27ea..5316336e 100644 --- a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs @@ -61,6 +61,9 @@ internal class QResyncSynchronizer : ImapSynchronizationStrategyBase // Perform QRESYNC synchronization. var localHighestModSeq = (ulong)folder.HighestModeSeq; + // HIGHESTMODSEQ must be a positive integer, 0 is illegal. + // It's harmless to set it to 1, as RFC-compliant server without mod-seq would ignore this parameter. + if (localHighestModSeq == 0) localHighestModSeq = 1; remoteFolder.MessagesVanished += OnMessagesVanished; remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged; @@ -115,6 +118,7 @@ internal class QResyncSynchronizer : ImapSynchronizationStrategyBase internal override async Task> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) { var localHighestModSeq = (ulong)Folder.HighestModeSeq; + if (localHighestModSeq == 0) localHighestModSeq = 1; return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); } } diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs index 00453f05..7b1a3071 100644 --- a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs +++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -20,6 +21,7 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel { private readonly IMailDialogService _dialogService; private readonly IAccountService _accountService; + private readonly ISmimeCertificateService _smimeCertificateService; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))] @@ -31,10 +33,12 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false; public AliasManagementPageViewModel(IMailDialogService dialogService, - IAccountService accountService) + IAccountService accountService, + ISmimeCertificateService smimeCertificateService) { _dialogService = dialogService; _accountService = accountService; + _smimeCertificateService = smimeCertificateService; } public override async void OnNavigatedTo(NavigationMode mode, object parameters) @@ -51,7 +55,22 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel private async Task LoadAliasesAsync() { - AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id); + var aliases = await _accountService.GetAccountAliasesAsync(Account.Id); + foreach (var alias in aliases) + { + alias.Certificates.Clear(); + alias.Certificates.Add(null); // First blank optioon + var certs = _smimeCertificateService.GetCertificates() + .Where(cert => cert.Subject.Contains(alias.AliasAddress, StringComparison.OrdinalIgnoreCase)) + .ToList(); + foreach (var cert in certs) + alias.Certificates.Add(cert); + + alias.SelectedSigningCertificate = !string.IsNullOrEmpty(alias.SelectedSigningCertificateThumbprint) + ? alias.Certificates.FirstOrDefault(c => c?.Thumbprint == alias.SelectedSigningCertificateThumbprint) + : null; + } + AccountAliases = aliases; } [RelayCommand] @@ -148,4 +167,20 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel await _accountService.DeleteAccountAliasAsync(alias.Id); await LoadAliasesAsync(); } + + public async Task SetAliasSmimeEncryption(MailAccountAlias alias, bool value) + { + alias.IsSmimeEncryptionEnabled = value; + await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases); + await LoadAliasesAsync(); + } + + public async Task SetSelectedSigningCertificate(MailAccountAlias alias, X509Certificate2 cert) + { + alias.SelectedSigningCertificate = cert; + alias.SelectedSigningCertificateThumbprint = cert?.Thumbprint; + + await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases); + await LoadAliasesAsync(); + } } diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index c058ca67..3e8ac098 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using MimeKit; +using MimeKit.Cryptography; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; @@ -68,19 +70,26 @@ public partial class ComposePageViewModel : MailBaseViewModel private MailAccount composingAccount; [ObservableProperty] - private List availableAliases; + public partial List AvailableAliases { get; set; } + [ObservableProperty] + public partial MailAccountAlias SelectedAlias { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverComposerGrid { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverFilesDropZone { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverImagesDropZone { get; set; } + [ObservableProperty] + public partial bool IsSmimeSignatureEnabled { get; set; } + [ObservableProperty] + public partial bool IsSmimeEncryptionEnabled { get; set; } [ObservableProperty] - private MailAccountAlias selectedAlias; + public partial X509Certificate2 SelectedSigningCertificate { get; set; } - [ObservableProperty] - private bool isDraggingOverComposerGrid; + public ObservableCollection AvailableCertificates = []; - [ObservableProperty] - private bool isDraggingOverFilesDropZone; - - [ObservableProperty] - private bool isDraggingOverImagesDropZone; + public bool AreCertificatesAvailable => AvailableCertificates.Count > 0; public ObservableCollection IncludedAttachments { get; set; } = []; public ObservableCollection Accounts { get; set; } = []; @@ -102,6 +111,7 @@ public partial class ComposePageViewModel : MailBaseViewModel public readonly IFontService FontService; public readonly IPreferencesService PreferencesService; public readonly IContactService ContactService; + public readonly ISmimeCertificateService _smimeCertificateService; public ComposePageViewModel(IMailDialogService dialogService, IMailService mailService, @@ -113,7 +123,8 @@ public partial class ComposePageViewModel : MailBaseViewModel IWinoRequestDelegator worker, IContactService contactService, IFontService fontService, - IPreferencesService preferencesService) + IPreferencesService preferencesService, + ISmimeCertificateService smimeCertificateService) { NativeAppService = nativeAppService; ContactService = contactService; @@ -127,6 +138,38 @@ public partial class ComposePageViewModel : MailBaseViewModel _fileService = fileService; _accountService = accountService; _worker = worker; + _smimeCertificateService = smimeCertificateService; + + foreach (var cert in _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias?.AliasAddress)) + { + if (cert != null) + { + AvailableCertificates.Add(cert); + } + } + } + + partial void OnSelectedAliasChanged(MailAccountAlias value) + { + if (value != null) + { + IsSmimeSignatureEnabled = value.SelectedSigningCertificateThumbprint != null; + IsSmimeEncryptionEnabled = value.IsSmimeEncryptionEnabled; + + AvailableCertificates.Clear(); + var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress); + foreach (var cert in certs) + { + AvailableCertificates.Add(cert); + } + SelectedSigningCertificate = AvailableCertificates + .Where(c => c.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint).FirstOrDefault() ?? AvailableCertificates.FirstOrDefault(); + } + } + + partial void OnSelectedSigningCertificateChanged(X509Certificate2 value) + { + IsSmimeSignatureEnabled = value != null; } [RelayCommand] @@ -213,6 +256,47 @@ public partial class ComposePageViewModel : MailBaseViewModel var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount; var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); + + // Load alias certs + var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress); + + if (IsSmimeSignatureEnabled) + { + var signingCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedSigningCertificateThumbprint) + ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint) + : null; + + var signer = new CmsSigner(signingCertificate) { DigestAlgorithm = DigestAlgorithm.Sha1 }; + + if (IsSmimeEncryptionEnabled) + { + var recipients = new CmsRecipientCollection(); + var cmsRecipients = CurrentMimeMessage.To.Mailboxes + .Select(mailbox => new CmsRecipient( + _smimeCertificateService.GetCertificates(emailAddress: mailbox.Address).FirstOrDefault() ?? _smimeCertificateService.GetCertificates(StoreName.AddressBook, emailAddress: mailbox.Address).FirstOrDefault() + )); + foreach (var recipient in cmsRecipients) + { + recipients.Add(recipient); + } + + CurrentMimeMessage.Body = ApplicationPkcs7Mime.SignAndEncrypt(signer, recipients, CurrentMimeMessage.Body); + } + else + { + // CurrentMimeMessage.Body = MultipartSigned.Create(signer, CurrentMimeMessage.Body); + CurrentMimeMessage.Body = ApplicationPkcs7Mime.Sign(signer, CurrentMimeMessage.Body); + } + } + else if (IsSmimeEncryptionEnabled) + { + // var encryptionCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedEncryptionCertificateThumbprint) + // ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedEncryptionCertificateThumbprint) + // : null; + // Encrypt the message if encryption certificate is selected. + CurrentMimeMessage.Body = ApplicationPkcs7Mime.Encrypt(CurrentMimeMessage.To.Mailboxes, CurrentMimeMessage.Body); + } + using MemoryStream memoryStream = new(); CurrentMimeMessage.WriteTo(FormatOptions.Default, memoryStream); byte[] buffer = memoryStream.GetBuffer(); diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 2ff81ead..7f3f8078 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -11,6 +11,7 @@ using CommunityToolkit.Mvvm.Messaging; using MailKit; using MimeKit; +using MimeKit.Cryptography; using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; @@ -62,7 +63,11 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100); public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false; + public bool IsSmimeSigned => (CurrentRenderModel?.Signatures?.Count ?? 0) > 0; + public bool IsSmimeEncrypted => CurrentRenderModel?.IsSmimeEncrypted ?? false; public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk; + public bool SmimeSignaturesValid => CurrentRenderModel?.Signatures?.Any(x => x.Value) ?? false; + public bool SmimeSignaturesInvalid => !SmimeSignaturesValid; public bool IsImageRenderingDisabled { @@ -102,6 +107,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanUnsubscribe))] + [NotifyPropertyChangedFor(nameof(IsSmimeSigned))] + [NotifyPropertyChangedFor(nameof(IsSmimeEncrypted))] + [NotifyPropertyChangedFor(nameof(SmimeSignaturesValid))] + [NotifyPropertyChangedFor(nameof(SmimeSignaturesInvalid))] public partial MailRenderModel CurrentRenderModel { get; set; } [ObservableProperty] @@ -132,19 +141,19 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public IPrintService PrintService { get; } public MailRenderingPageViewModel(IMailDialogService dialogService, - INativeAppService nativeAppService, - IUnderlyingThemeService underlyingThemeService, - IMimeFileService mimeFileService, - IMailService mailService, - IFileService fileService, - IWinoRequestDelegator requestDelegator, - IStatePersistanceService statePersistenceService, - IContactService contactService, - IClipboardService clipboardService, - IUnsubscriptionService unsubscriptionService, - IPreferencesService preferencesService, - IPrintService printService, - IApplicationConfiguration applicationConfiguration) + INativeAppService nativeAppService, + IUnderlyingThemeService underlyingThemeService, + IMimeFileService mimeFileService, + IMailService mailService, + IFileService fileService, + IWinoRequestDelegator requestDelegator, + IStatePersistanceService statePersistenceService, + IContactService contactService, + IClipboardService clipboardService, + IUnsubscriptionService unsubscriptionService, + IPreferencesService preferencesService, + IPrintService printService, + IApplicationConfiguration applicationConfiguration) { _dialogService = dialogService; NativeAppService = nativeAppService; @@ -716,8 +725,8 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, if (isSaved) { _dialogService.InfoBarMessage(Translator.Info_PDFSaveSuccessTitle, - string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath), - InfoBarMessageType.Success); + string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath), + InfoBarMessageType.Success); } } catch (Exception ex) @@ -800,6 +809,72 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, }); } + [RelayCommand] + private async Task ShowSmimeSigningCertificateInfoAsync() + { + if (IsSmimeSigned) + { + MimePart signaturePart; + if (initializedMimeMessageInformation?.MimeMessage?.Body is MultipartSigned signed && signed[1] is MimePart signaturePart1) + { + signaturePart = signaturePart1; + } + else if (initializedMimeMessageInformation?.MimeMessage?.Body is ApplicationPkcs7Mime pkcs7) + { + signaturePart = null; + } + else + { + //_dialogService.InfoBarMessage(Translator.Info_SmimeSignatureNotFoundTitle, Translator.Info_SmimeSignatureNotFoundMessage, InfoBarMessageType.Error); + return; + } + + string info = $"{Translator.SmimeSignaturesInMessage}:\n"; + foreach (var (signature, valid) in CurrentRenderModel.Signatures) + { + info += string.Format(Translator.SmimeSignatureEntry, valid ? "✅" : "❌", signature.SignerCertificate.Name, signature.SignerCertificate.Fingerprint, signature.SignerCertificate.CreationDate, signature.SignerCertificate.ExpirationDate); + } + await ShowSmimeCertificateInfoAsync(signaturePart, info, Translator.SmimeSigningCertificateInfoTitle); + } + + } + + private async Task ShowSmimeCertificateInfoAsync(MimePart certificateAttachment, string additionalInfo = "", string title = null) + { + { + if (certificateAttachment == null) + { + await _dialogService.ShowConfirmationDialogAsync( + $"{additionalInfo}\n{Translator.SmimeNoCertificateFileFound}", title ?? Translator.SmimeCertificateInfoTitle, Translator.Buttons_OK); + return; + } + var fileName = certificateAttachment.FileName ?? "smime.p7s"; + var contentType = certificateAttachment.ContentType?.MimeType ?? "application/pkcs7-signature"; + var size = certificateAttachment.Content?.Stream?.Length ?? 0; + var info = string.Format(Translator.SmimeCertificateFileInfo, fileName, contentType, size); + + var result = await _dialogService.ShowConfirmationDialogAsync( + $"{additionalInfo}\n{info}", title ?? Translator.SmimeCertificateInfoTitle, + Translator.SmimeSaveCertificate); + if (result) + { + var pickedPath = await _dialogService.PickFilePathAsync(fileName); + if (!string.IsNullOrEmpty(pickedPath)) + { + var pickedDirectory = Path.GetDirectoryName(pickedPath); + var pickedFileName = Path.GetFileName(pickedPath); + await using (var stream = await _fileService.GetFileStreamAsync(pickedDirectory, pickedFileName)) + { + await certificateAttachment.Content!.DecodeToAsync(stream); + } + + _dialogService.InfoBarMessage(Translator.SmimeCertificate, string.Format(Translator.SmimeCertificateSavedTo, pickedPath), + InfoBarMessageType.Success); + } + } + } + } + protected override void RegisterRecipients() { base.RegisterRecipients(); diff --git a/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs b/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs new file mode 100644 index 00000000..3eba7180 --- /dev/null +++ b/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Common; + +namespace Wino.Mail.ViewModels; + +public partial class SignatureAndEncryptionPageViewModel : MailBaseViewModel +{ + private readonly ISmimeCertificateService _smimeCertificateService; + private readonly IDialogServiceBase _dialogService; + private readonly IFileService _fileService; + + public ObservableCollection PersonalCertificates { get; } = []; + public ObservableCollection RecipientCertificates { get; } = []; + public List SelectedPersonalCertificates { get; } = []; + public List SelectedRecipientCertificates { get; } = []; + + public bool PersonalCertificatesEmpty => PersonalCertificates.Count == 0; + + public SignatureAndEncryptionPageViewModel( + IDialogServiceBase dialogService, + ISmimeCertificateService smimeCertificateService, + IFileService fileService + ) + { + _dialogService = dialogService; + _fileService = fileService; + _smimeCertificateService = smimeCertificateService; + + PersonalCertificates.CollectionChanged += (s, e) => { OnPropertyChanged(nameof(PersonalCertificatesEmpty)); }; + LoadAllCertificates(); + } + + private void LoadAllCertificates() + { + PersonalCertificates.Clear(); + var personalCerts = _smimeCertificateService.GetCertificates(); + foreach (var cert in personalCerts) + { + PersonalCertificates.Add(cert); + } + + // Recipient certificates + RecipientCertificates.Clear(); + var recipientCerts = _smimeCertificateService.GetCertificates(storeName: StoreName.AddressBook); + foreach (var cert in recipientCerts) + { + RecipientCertificates.Add(cert); + } + } + + [RelayCommand] + public async Task ImportPersonalCertificatesAsync() + { + await ImportCertificates(StoreName.My); + } + + [RelayCommand] + public async Task ImportRecipientCertificatesAsync() + { + await ImportCertificates(StoreName.AddressBook); + } + + private async Task ImportCertificates(StoreName storeName) + { + var files = await PickCertificateFilesAsync(); + var failedImports = new List(); + var successCount = 0; + foreach (var file in files) + { + string password = null; + if (file.FileExtension.Equals(".pfx") || file.FileExtension.Equals(".p12")) + { + password = await PromptForPasswordAsync(file.FileName); + } + + try + { + _smimeCertificateService.ImportCertificate(file.FileExtension, file.Data, password, + storeName: storeName); + successCount++; + } + catch (Exception ex) + { + failedImports.Add($"{file.FileName}: {ex.Message}"); + } + } + LoadAllCertificates(); + if (successCount > 0) + { + _dialogService.InfoBarMessage( + string.Format(Translator.Smime_ImportCertificates_Success), + Translator.GeneralTitle_Info, + InfoBarMessageType.Success); + } + if (failedImports.Count > 0) + { + await _dialogService.ShowMessageAsync( + $"{Translator.Smime_ImportCertificates_Error}\n\n{string.Join("\n", failedImports)}", + Translator.GeneralTitle_Warning, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning); + } + } + + [RelayCommand] + public async Task RemovePersonalCertificatesAsync() + { + await RemoveCertificatesAsync(SelectedPersonalCertificates, StoreName.My); + } + + [RelayCommand] + public async Task RemoveRecipientCertificatesAsync() + { + await RemoveCertificatesAsync(SelectedRecipientCertificates, StoreName.AddressBook); + } + + private async Task RemoveCertificatesAsync(List certificates, StoreName storeName) + { + if (certificates.Any()) + { + var confirm = await ConfirmAsync(string.Format(Translator.Smime_RemoveCertificates_Confirm, + string.Join(", ", certificates.Select(cert => cert.Subject)))); + if (confirm) + { + foreach (var cert in certificates) + { + _smimeCertificateService.RemoveCertificate(cert.Thumbprint, storeName: storeName); + } + + LoadAllCertificates(); + _dialogService.InfoBarMessage( + Translator.Smime_RemoveCertificates_Success, + Translator.GeneralTitle_Info, + InfoBarMessageType.Success + ); + } + } + } + + [RelayCommand] + public async Task ExportPersonalCertificatesAsync() + { + await ExportCertificatesAsync(SelectedPersonalCertificates); + } + + [RelayCommand] + public async Task ExportRecipientCertificatesAsync() + { + await ExportCertificatesAsync(SelectedRecipientCertificates); + } + + // Export logic for .cer or .pem + private async Task ExportCertificatesAsync(IEnumerable cert) + { + var failedExports = new List(); + var successCount = 0; + foreach (var certificate in cert) + { + var fileName = $"{certificate.Subject.Replace("CN=", "")}.cer"; + var path = await _dialogService.PickFilePathAsync(fileName); + if (path != null) + { + var folderPath = System.IO.Path.GetDirectoryName(path); + await using var stream = await _fileService.GetFileStreamAsync(folderPath, fileName); + if (stream != null) + { + try + { + var certificateData = certificate.Export(X509ContentType.Cert); + await stream.WriteAsync(certificateData, 0, certificateData.Length); + await stream.FlushAsync(); + successCount++; + } + catch (Exception ex) + { + failedExports.Add($"{certificate.Subject}: {ex.Message}"); + } + } + else + { + failedExports.Add($"{certificate.Subject}: File stream error"); + } + } + } + if (successCount > 0) + { + _dialogService.InfoBarMessage( + Translator.Smime_ExportCertificates_Success, + Translator.GeneralTitle_Info, + InfoBarMessageType.Success + ); + } + if (failedExports.Count > 0) + { + await _dialogService.ShowMessageAsync( + $"{Translator.Smime_ExportCertificates_Error}\n\n{string.Join("\n", failedExports)}", + Translator.GeneralTitle_Warning, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning); + } + } + + private async Task ShowCertificateDetailsAsync(X509Certificate2 cert) + { + var details = string.Format(Translator.Smime_CertificateDetails, cert.Subject, cert.Issuer, cert.NotBefore, + cert.NotAfter, cert.Thumbprint); + await _dialogService.ShowMessageAsync(details, Translator.GeneralTitle_Info, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Information); + } + + // Confirmation dialog + private async Task ConfirmAsync(string message) + { + return await _dialogService.ShowConfirmationDialogAsync(message, Translator.Smime_Confirm_Title, + Translator.Buttons_Yes); + } + + // File picker for importing certificates + private async Task> PickCertificateFilesAsync() + { + return await _dialogService.PickFilesAsync(".pfx", ".p12", ".cer", ".crt"); + } + + // Ask for password for .pfx/.p12 + private async Task PromptForPasswordAsync(string fileName) + { + return await _dialogService.ShowTextInputDialogAsync("", + Translator.Smime_CertificatePassword_Title, + string.Format(Translator.Smime_CertificatePassword_Placeholder, fileName), Translator.Buttons_OK); + } +} diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj index 90c6a5f2..b84d5cb7 100644 --- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj +++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj @@ -13,6 +13,7 @@ + diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 762142a0..052c5a75 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -8,15 +8,15 @@ 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.WinUI; -using Wino.Mail.WinUI.Interfaces; 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; @@ -31,6 +31,7 @@ public partial class App : WinoApplication, IRecipient(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddTransient(); diff --git a/Wino.Mail.WinUI/Services/DialogServiceBase.cs b/Wino.Mail.WinUI/Services/DialogServiceBase.cs index dd101384..f4ce9cb4 100644 --- a/Wino.Mail.WinUI/Services/DialogServiceBase.cs +++ b/Wino.Mail.WinUI/Services/DialogServiceBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; @@ -15,9 +16,9 @@ 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.Dialogs; using Wino.Messaging.Client.Shell; using WinRT.Interop; @@ -48,7 +49,7 @@ public class DialogServiceBase : IDialogServiceBase { var picker = new FolderPicker() { - SuggestedStartLocation = PickerLocationId.Desktop + SuggestedStartLocation = PickerLocationId.Desktop, }; picker.FileTypeFilter.Add("*"); @@ -61,7 +62,7 @@ public class DialogServiceBase : IDialogServiceBase StorageApplicationPermissions.FutureAccessList.Add(folder); - return folder.Path; + return $"{Path.Combine(folder.Path, saveFileName)}"; //var picker = new FileSavePicker //{ diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index b8575d68..684740fc 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -61,6 +61,7 @@ public class NavigationService : NavigationServiceBase, INavigationService WinoPage.EditAccountDetailsPage => typeof(EditAccountDetailsPage), WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage), WinoPage.ContactsPage => typeof(ContactsPage), + WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage), _ => null, }; } diff --git a/Wino.Mail.WinUI/Services/SettingsBuilderService.cs b/Wino.Mail.WinUI/Services/SettingsBuilderService.cs index d7dc6a7d..91d01f62 100644 --- a/Wino.Mail.WinUI/Services/SettingsBuilderService.cs +++ b/Wino.Mail.WinUI/Services/SettingsBuilderService.cs @@ -18,6 +18,7 @@ public class SettingsBuilderService : ISettingsBuilderService new SettingOption(Translator.Settings_KeyboardShortcuts_Title, Translator.Settings_KeyboardShortcuts_Description, WinoPage.KeyboardShortcutsPage,"F1 M 2.451172 16.25 C 2.125651 16.25 1.814779 16.18327 1.518555 16.049805 C 1.222331 15.916342 0.961914 15.737305 0.737305 15.512695 C 0.512695 15.288086 0.333659 15.02767 0.200195 14.731445 C 0.066732 14.435222 0 14.12435 0 13.798828 L 0 4.951172 C 0 4.625651 0.066732 4.314779 0.200195 4.018555 C 0.333659 3.722332 0.512695 3.461914 0.737305 3.237305 C 0.961914 3.012695 1.222331 2.83366 1.518555 2.700195 C 1.814779 2.566732 2.125651 2.5 2.451172 2.5 L 17.548828 2.5 C 17.874348 2.5 18.185221 2.566732 18.481445 2.700195 C 18.777668 2.83366 19.038086 3.012695 19.262695 3.237305 C 19.487305 3.461914 19.66634 3.722332 19.799805 4.018555 C 19.933268 4.314779 20 4.625651 20 4.951172 L 20 13.798828 C 20 14.12435 19.933268 14.435222 19.799805 14.731445 C 19.66634 15.02767 19.487305 15.288086 19.262695 15.512695 C 19.038086 15.737305 18.777668 15.916342 18.481445 16.049805 C 18.185221 16.18327 17.874348 16.25 17.548828 16.25 Z M 17.5 15 C 17.675781 15 17.838541 14.967448 17.988281 14.902344 C 18.13802 14.83724 18.269855 14.747722 18.383789 14.633789 C 18.497721 14.519857 18.587238 14.388021 18.652344 14.238281 C 18.717447 14.088542 18.75 13.925781 18.75 13.75 L 18.75 5 C 18.75 4.830729 18.717447 4.669597 18.652344 4.516602 C 18.587238 4.363607 18.497721 4.230144 18.383789 4.116211 C 18.269855 4.002279 18.136393 3.912762 17.983398 3.847656 C 17.830402 3.782553 17.66927 3.75 17.5 3.75 L 2.5 3.75 C 2.324219 3.75 2.161458 3.782553 2.011719 3.847656 C 1.861979 3.912762 1.730143 4.002279 1.616211 4.116211 C 1.502279 4.230144 1.41276 4.361979 1.347656 4.511719 C 1.282552 4.661459 1.25 4.82422 1.25 5 L 1.25 13.75 C 1.25 13.925781 1.282552 14.09017 1.347656 14.243164 C 1.41276 14.396159 1.500651 14.527995 1.611328 14.638672 C 1.722005 14.74935 1.853841 14.83724 2.006836 14.902344 C 2.159831 14.967448 2.324219 15 2.5 15 Z M 3.125 6.5625 C 3.125 6.302084 3.216146 6.08073 3.398438 5.898438 C 3.580729 5.716146 3.802083 5.625 4.0625 5.625 C 4.322917 5.625 4.544271 5.716146 4.726562 5.898438 C 4.908854 6.08073 5 6.302084 5 6.5625 C 5 6.822917 4.908854 7.044271 4.726562 7.226562 C 4.544271 7.408854 4.322917 7.5 4.0625 7.5 C 3.802083 7.5 3.580729 7.408854 3.398438 7.226562 C 3.216146 7.044271 3.125 6.822917 3.125 6.5625 Z M 6.875 6.5625 C 6.875 6.302084 6.966146 6.08073 7.148438 5.898438 C 7.330729 5.716146 7.552083 5.625 7.8125 5.625 C 8.072916 5.625 8.294271 5.716146 8.476562 5.898438 C 8.658854 6.08073 8.75 6.302084 8.75 6.5625 C 8.75 6.822917 8.658854 7.044271 8.476562 7.226562 C 8.294271 7.408854 8.072916 7.5 7.8125 7.5 C 7.552083 7.5 7.330729 7.408854 7.148438 7.226562 C 6.966146 7.044271 6.875 6.822917 6.875 6.5625 Z M 10.625 6.5625 C 10.625 6.302084 10.716146 6.08073 10.898438 5.898438 C 11.080729 5.716146 11.302083 5.625 11.5625 5.625 C 11.822916 5.625 12.044271 5.716146 12.226562 5.898438 C 12.408854 6.08073 12.5 6.302084 12.5 6.5625 C 12.5 6.822917 12.408854 7.044271 12.226562 7.226562 C 12.044271 7.408854 11.822916 7.5 11.5625 7.5 C 11.302083 7.5 11.080729 7.408854 10.898438 7.226562 C 10.716146 7.044271 10.625 6.822917 10.625 6.5625 Z M 14.375 6.5625 C 14.375 6.302084 14.466146 6.08073 14.648438 5.898438 C 14.830729 5.716146 15.052083 5.625 15.3125 5.625 C 15.572916 5.625 15.79427 5.716146 15.976562 5.898438 C 16.158854 6.08073 16.25 6.302084 16.25 6.5625 C 16.25 6.822917 16.158854 7.044271 15.976562 7.226562 C 15.79427 7.408854 15.572916 7.5 15.3125 7.5 C 15.052083 7.5 14.830729 7.408854 14.648438 7.226562 C 14.466146 7.044271 14.375 6.822917 14.375 6.5625 Z M 5.625 10.3125 C 5.625 10.052084 5.716146 9.830729 5.898438 9.648438 C 6.080729 9.466146 6.302083 9.375 6.5625 9.375 C 6.822917 9.375 7.044271 9.466146 7.226562 9.648438 C 7.408854 9.830729 7.5 10.052084 7.5 10.3125 C 7.5 10.572917 7.408854 10.794271 7.226562 10.976562 C 7.044271 11.158854 6.822917 11.25 6.5625 11.25 C 6.302083 11.25 6.080729 11.158854 5.898438 10.976562 C 5.716146 10.794271 5.625 10.572917 5.625 10.3125 Z M 9.375 10.3125 C 9.375 10.052084 9.466146 9.830729 9.648438 9.648438 C 9.830729 9.466146 10.052083 9.375 10.3125 9.375 C 10.572916 9.375 10.794271 9.466146 10.976562 9.648438 C 11.158854 9.830729 11.25 10.052084 11.25 10.3125 C 11.25 10.572917 11.158854 10.794271 10.976562 10.976562 C 10.794271 11.158854 10.572916 11.25 10.3125 11.25 C 10.052083 11.25 9.830729 11.158854 9.648438 10.976562 C 9.466146 10.794271 9.375 10.572917 9.375 10.3125 Z M 13.125 10.3125 C 13.124999 10.052084 13.216145 9.830729 13.398438 9.648438 C 13.580729 9.466146 13.802083 9.375 14.0625 9.375 C 14.322916 9.375 14.544271 9.466146 14.726562 9.648438 C 14.908854 9.830729 14.999999 10.052084 15 10.3125 C 14.999999 10.572917 14.908854 10.794271 14.726562 10.976562 C 14.544271 11.158854 14.322916 11.25 14.0625 11.25 C 13.802083 11.25 13.580729 11.158854 13.398438 10.976562 C 13.216145 10.794271 13.124999 10.572917 13.125 10.3125 Z M 4.375 13.75 C 4.205729 13.75 4.059245 13.688151 3.935547 13.564453 C 3.811849 13.440756 3.75 13.294271 3.75 13.125 C 3.75 12.955729 3.811849 12.809245 3.935547 12.685547 C 4.059245 12.56185 4.205729 12.5 4.375 12.5 L 15.625 12.5 C 15.79427 12.5 15.940754 12.56185 16.064453 12.685547 C 16.18815 12.809245 16.25 12.955729 16.25 13.125 C 16.25 13.294271 16.18815 13.440756 16.064453 13.564453 C 15.940754 13.688151 15.79427 13.75 15.625 13.75 Z "), new SettingOption(Translator.SettingsMessageList_Title, Translator.SettingsMessageList_Description, WinoPage.MessageListPage,"F1 M 20 1.25 L 20 12.5 L 18.75 12.5 L 18.75 2.5 L 1.25 2.5 L 1.25 13.75 L 3.75 13.75 L 3.75 16.611328 L 6.611328 13.75 L 13.75 13.75 L 13.75 15 L 7.138672 15 L 2.5 19.638672 L 2.5 15 L 0 15 L 0 1.25 Z M 16.875 13.75 L 20 16.875 L 16.875 20 L 15.986328 19.111328 L 17.607422 17.5 L 11.25 17.5 L 11.25 16.25 L 17.607422 16.25 L 15.986328 14.638672 Z "), new SettingOption(Translator.SettingsReadComposePane_Title, Translator.SettingsReadComposePane_Description, WinoPage.ReadComposePanePage,"F1 M 20 2.5 L 20 3.75 L 8.75 3.75 L 8.75 2.5 Z M 17.5 15 L 0 15 L 0 13.75 L 17.5 13.75 Z M 2.5 10 L 20 10 L 20 11.25 L 2.5 11.25 Z M 2.5 17.5 L 20 17.5 L 20 18.75 L 2.5 18.75 Z M 3.125 7.5 C 2.695312 7.5 2.291667 7.416992 1.914062 7.250977 C 1.536458 7.084961 1.206055 6.860352 0.922852 6.577148 C 0.639648 6.293945 0.415039 5.963542 0.249023 5.585938 C 0.083008 5.208334 0 4.804688 0 4.375 C 0 3.945312 0.083008 3.541668 0.249023 3.164062 C 0.415039 2.786459 0.639648 2.456055 0.922852 2.172852 C 1.206055 1.889648 1.536458 1.665039 1.914062 1.499023 C 2.291667 1.333008 2.695312 1.25 3.125 1.25 C 3.554688 1.25 3.958333 1.333008 4.335938 1.499023 C 4.713542 1.665039 5.043945 1.889648 5.327148 2.172852 C 5.610352 2.456055 5.834961 2.786459 6.000977 3.164062 C 6.166992 3.541668 6.25 3.945312 6.25 4.375 L 5 4.375 C 5 4.114584 4.951172 3.870443 4.853516 3.642578 C 4.755859 3.414715 4.622396 3.216146 4.453125 3.046875 C 4.283854 2.877605 4.085286 2.744141 3.857422 2.646484 C 3.629557 2.548828 3.385417 2.5 3.125 2.5 C 2.864583 2.5 2.620443 2.548828 2.392578 2.646484 C 2.164713 2.744141 1.966146 2.877605 1.796875 3.046875 C 1.627604 3.216146 1.494141 3.414715 1.396484 3.642578 C 1.298828 3.870443 1.25 4.114584 1.25 4.375 C 1.25 4.635418 1.298828 4.879559 1.396484 5.107422 C 1.494141 5.335287 1.627604 5.533854 1.796875 5.703125 C 1.966146 5.872396 2.164713 6.005859 2.392578 6.103516 C 2.620443 6.201172 2.864583 6.25 3.125 6.25 L 17.5 6.25 L 17.5 7.5 Z "), + new SettingOption(Translator.SettingsSignatureAndEncryption_Title, Translator.SettingsSignatureAndEncryption_Description, WinoPage.SignatureAndEncryptionPage, "M2 5.75C2 4.23122 3.23122 3 4.75 3H19.25C20.7688 3 22 4.23122 22 5.75V15.25C22 16.7688 20.7688 18 19.25 18H10V17.0005C10.12 16.8408 10.2306 16.6737 10.3311 16.5H19.25C19.9404 16.5 20.5 15.9404 20.5 15.25V5.75C20.5 5.05964 19.9404 4.5 19.25 4.5H4.75C4.05964 4.5 3.5 5.05964 3.5 5.75V9.66891C2.91464 10.0075 2.40429 10.4614 2 10.9995V5.75ZM6.75 7C6.33579 7 6 7.33579 6 7.75C6 8.16421 6.33579 8.5 6.75 8.5H17.25C17.6642 8.5 18 8.16421 18 7.75C18 7.33579 17.6642 7 17.25 7H6.75ZM12.75 12C12.3358 12 12 12.3358 12 12.75C12 13.1642 12.3358 13.5 12.75 13.5H17.25C17.6642 13.5 18 13.1642 18 12.75C18 12.3358 17.6642 12 17.25 12H12.75ZM5.99967 10C3.79017 10 1.99902 11.7911 1.99902 14.0006C1.99902 16.2101 3.79017 18.0013 5.99967 18.0013C8.20916 18.0013 10.0003 16.2101 10.0003 14.0006C10.0003 11.7911 8.20916 10 5.99967 10ZM9.00076 18.001C8.16487 18.6291 7.12573 19.0013 5.99967 19.0013C4.8745 19.0013 3.83612 18.6297 3.00058 18.0025L3.0001 21.2487C3.0001 21.8195 3.6046 22.1681 4.09019 21.9176L4.17966 21.8635L6.00002 20.5912L7.81967 21.8635C8.28757 22.1904 8.91959 21.8946 8.99232 21.353L8.99923 21.2487L9.00076 18.001Z"), new SettingOption(Translator.SettingsAbout_Title, Translator.SettingsAbout_Description, WinoPage.AboutPage,"F1 M 9.375 18.75 C 8.509114 18.75 7.677409 18.639322 6.879883 18.417969 C 6.082356 18.196615 5.335286 17.882486 4.638672 17.475586 C 3.942057 17.068686 3.308919 16.580404 2.739258 16.010742 C 2.169596 15.441081 1.681315 14.807943 1.274414 14.111328 C 0.867513 13.414714 0.553385 12.667644 0.332031 11.870117 C 0.110677 11.072592 0 10.240886 0 9.375 C 0 8.509115 0.110677 7.677409 0.332031 6.879883 C 0.553385 6.082357 0.867513 5.335287 1.274414 4.638672 C 1.681315 3.942059 2.169596 3.30892 2.739258 2.739258 C 3.308919 2.169598 3.942057 1.681316 4.638672 1.274414 C 5.335286 0.867514 6.082356 0.553387 6.879883 0.332031 C 7.677409 0.110678 8.509114 0 9.375 0 C 10.240885 0 11.072591 0.110678 11.870117 0.332031 C 12.667643 0.553387 13.414713 0.867514 14.111328 1.274414 C 14.807942 1.681316 15.44108 2.169598 16.010742 2.739258 C 16.580402 3.30892 17.068684 3.942059 17.475586 4.638672 C 17.882486 5.335287 18.196613 6.082357 18.417969 6.879883 C 18.639322 7.677409 18.75 8.509115 18.75 9.375 C 18.75 10.240886 18.639322 11.072592 18.417969 11.870117 C 18.196613 12.667644 17.882486 13.414714 17.475586 14.111328 C 17.068684 14.807943 16.580402 15.441081 16.010742 16.010742 C 15.44108 16.580404 14.807942 17.068686 14.111328 17.475586 C 13.414713 17.882486 12.667643 18.196615 11.870117 18.417969 C 11.072591 18.639322 10.240885 18.75 9.375 18.75 Z M 9.375 1.25 C 8.626302 1.25 7.906901 1.347656 7.216797 1.542969 C 6.526692 1.738281 5.880533 2.011719 5.27832 2.363281 C 4.676106 2.714844 4.127604 3.138021 3.632812 3.632812 C 3.138021 4.127604 2.714844 4.676107 2.363281 5.27832 C 2.011719 5.880534 1.738281 6.52832 1.542969 7.22168 C 1.347656 7.915039 1.25 8.632812 1.25 9.375 C 1.25 10.117188 1.347656 10.834961 1.542969 11.52832 C 1.738281 12.22168 2.011719 12.869467 2.363281 13.47168 C 2.714844 14.073894 3.138021 14.622396 3.632812 15.117188 C 4.127604 15.611979 4.676106 16.035156 5.27832 16.386719 C 5.880533 16.738281 6.526692 17.011719 7.216797 17.207031 C 7.906901 17.402344 8.626302 17.5 9.375 17.5 C 10.117188 17.5 10.834961 17.402344 11.52832 17.207031 C 12.221679 17.011719 12.869465 16.738281 13.47168 16.386719 C 14.073893 16.035156 14.622396 15.611979 15.117188 15.117188 C 15.611979 14.622396 16.035156 14.073894 16.386719 13.47168 C 16.738281 12.869467 17.011719 12.223308 17.207031 11.533203 C 17.402344 10.8431 17.5 10.123698 17.5 9.375 C 17.5 8.632812 17.402344 7.915039 17.207031 7.22168 C 17.011719 6.52832 16.738281 5.880534 16.386719 5.27832 C 16.035156 4.676107 15.611979 4.127604 15.117188 3.632812 C 14.622396 3.138021 14.073893 2.714844 13.47168 2.363281 C 12.869465 2.011719 12.221679 1.738281 11.52832 1.542969 C 10.834961 1.347656 10.117188 1.25 9.375 1.25 Z M 8.75 7.5 L 10 7.5 L 10 13.75 L 8.75 13.75 Z M 8.75 5 L 10 5 L 10 6.25 L 8.75 6.25 Z "), }; } diff --git a/Wino.Mail.WinUI/Services/SmimeCertificateService.cs b/Wino.Mail.WinUI/Services/SmimeCertificateService.cs new file mode 100644 index 00000000..45351659 --- /dev/null +++ b/Wino.Mail.WinUI/Services/SmimeCertificateService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.WinUI.Services; + +public class SmimeCertificateService : ISmimeCertificateService +{ + private const string CertificateFriendlyName = "Wino Mail Certificate"; + + /// + /// Retrieves all personal certificates from the current user's certificate store. + /// + /// This method enumerates certificates in the current user's "My" certificate store that have a + /// private key and at least one extension. The store is opened in read-only mode. + /// An enumerable collection of objects representing the personal certificates that + /// meet the specified criteria. If no matching certificates are found, the collection will be empty. + public IEnumerable GetCertificates(StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser, string emailAddress = null) + { + using var store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadOnly); + var certs = store.Certificates.Where(cert => cert.FriendlyName == CertificateFriendlyName); + return emailAddress != null ? certs.Where(cert => cert.Subject.Contains(emailAddress, StringComparison.OrdinalIgnoreCase)) : certs; + } + + + public void ImportCertificate(string fileExtension, byte[] rawData, string password = null, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser) + { + X509Certificate2Collection collection = []; + + if (fileExtension is ".p12" or ".pfx") + { + collection.AddRange(X509CertificateLoader.LoadPkcs12Collection(rawData, password, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)); + } else + { + collection.Add(X509CertificateLoader.LoadCertificate(rawData)); + } + + foreach (var cert in collection) + { + cert.FriendlyName = CertificateFriendlyName; + } + + using var store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadWrite); + store.AddRange(collection); + store.Close(); + } + + public void RemoveCertificate(string thumbprint, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser) + { + using var store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadWrite); + var cert = store.Certificates.FirstOrDefault(c => c.Thumbprint == thumbprint); + if (cert != null) + { + store.Remove(cert); + } + } +} diff --git a/Wino.Mail.WinUI/Views/Abstract/SignatureAndEncryptionPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/SignatureAndEncryptionPageAbstract.cs new file mode 100644 index 00000000..6c75ebf3 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Abstract/SignatureAndEncryptionPageAbstract.cs @@ -0,0 +1,8 @@ +using Wino.Mail.ViewModels; +using Wino.Mail.WinUI; + +namespace Wino.Views.Abstract; + +public abstract class SignatureAndEncryptionPageAbstract : BasePage +{ +} diff --git a/Wino.Mail.WinUI/Views/ComposePage.xaml b/Wino.Mail.WinUI/Views/ComposePage.xaml index 3ba5f612..0f918878 100644 --- a/Wino.Mail.WinUI/Views/ComposePage.xaml +++ b/Wino.Mail.WinUI/Views/ComposePage.xaml @@ -195,7 +195,7 @@ MinWidth="40" Click="{x:Bind WebViewEditor.ToggleEditorTheme}" LabelPosition="Collapsed" - ToolTipService.ToolTip="Light Theme" + ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_LightTheme}" Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay}"> @@ -207,7 +207,7 @@ MinWidth="40" Click="{x:Bind WebViewEditor.ToggleEditorTheme}" LabelPosition="Collapsed" - ToolTipService.ToolTip="Dark Theme" + ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_DarkTheme}" Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay, Converter={StaticResource ReverseBooleanToVisibilityConverter}}"> @@ -232,7 +232,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}" - Label="Bold"> + Label="{x:Bind domain:Translator.Composer_Bold}"> @@ -241,7 +241,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}" - Label="Italic"> + Label="{x:Bind domain:Translator.Composer_Italic}"> @@ -250,7 +250,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}" - Label="Underline"> + Label="{x:Bind domain:Translator.Composer_Underline}"> @@ -259,7 +259,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}" - Label="Stroke"> + Label="{x:Bind domain:Translator.Composer_Stroke}"> @@ -271,7 +271,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}" - Label="Bullet List"> + Label="{x:Bind domain:Translator.Composer_BulletList}"> @@ -281,7 +281,7 @@ Width="Auto" MinWidth="40" IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}" - Label="Ordered List"> + Label="{x:Bind domain:Translator.Composer_OrderedList}"> @@ -294,7 +294,7 @@ MinWidth="40" Click="{x:Bind WebViewEditor.EditorOutdentAsync}" IsEnabled="{x:Bind WebViewEditor.IsEditorOutdentEnabled, Mode=OneWay}" - Label="Outdent"> + Label="{x:Bind domain:Translator.Composer_Outdent}"> @@ -307,7 +307,7 @@ MinWidth="40" Click="{x:Bind WebViewEditor.EditorIndentAsync}" IsEnabled="{x:Bind WebViewEditor.IsEditorIndentEnabled, Mode=OneWay}" - Label="Indent"> + Label="{x:Bind domain:Translator.Composer_Indent}"> @@ -438,6 +438,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -525,7 +573,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Data="F1 M 3.75 4.902344 C 3.75 4.225262 3.885091 3.588867 4.155273 2.993164 C 4.425456 2.397461 4.790039 1.878256 5.249023 1.435547 C 5.708008 0.99284 6.238606 0.642904 6.84082 0.385742 C 7.443034 0.128582 8.079427 0 8.75 0 C 9.420572 0 10.056966 0.128582 10.65918 0.385742 C 11.261393 0.642904 11.791992 0.99284 12.250977 1.435547 C 12.709961 1.878256 13.074544 2.397461 13.344727 2.993164 C 13.614908 3.588867 13.75 4.225262 13.75 4.902344 C 13.75 5.332031 13.707682 5.742188 13.623047 6.132812 C 13.538411 6.523438 13.395182 6.907553 13.193359 7.285156 C 12.307942 7.434896 11.484375 7.734375 10.722656 8.183594 C 11.295572 7.819012 11.735025 7.353517 12.041016 6.787109 C 12.347005 6.220704 12.5 5.598959 12.5 4.921875 C 12.5 4.414062 12.399088 3.937176 12.197266 3.491211 C 11.995442 3.045248 11.723633 2.65625 11.381836 2.324219 C 11.040039 1.992188 10.641275 1.730145 10.185547 1.538086 C 9.729817 1.346029 9.251302 1.25 8.75 1.25 C 8.229166 1.25 7.740885 1.347656 7.285156 1.542969 C 6.829427 1.738281 6.432292 2.005209 6.09375 2.34375 C 5.755208 2.682293 5.488281 3.079428 5.292969 3.535156 C 5.097656 3.990887 5 4.479167 5 5 C 5 5.501303 5.096028 5.979818 5.288086 6.435547 C 5.480143 6.891276 5.742188 7.290039 6.074219 7.631836 C 6.40625 7.973633 6.795247 8.245443 7.241211 8.447266 C 7.687174 8.649089 8.164062 8.75 8.671875 8.75 C 9.023438 8.75 9.360352 8.709311 9.682617 8.62793 C 10.004883 8.54655 10.322266 8.417969 10.634766 8.242188 C 9.873047 8.704428 9.222005 9.290365 8.681641 10 L 8.662109 10 C 8.011067 10 7.389323 9.868164 6.796875 9.604492 C 6.204427 9.34082 5.681966 8.984375 5.229492 8.535156 C 4.777018 8.085938 4.417317 7.565104 4.150391 6.972656 C 3.883463 6.380209 3.75 5.755209 3.75 5.097656 Z M 8.75 14.375 C 8.75 13.600261 8.898111 12.871094 9.194336 12.1875 C 9.49056 11.503906 9.892578 10.908203 10.400391 10.400391 C 10.908203 9.892578 11.503906 9.490561 12.1875 9.194336 C 12.871093 8.898112 13.60026 8.75 14.375 8.75 C 14.889322 8.75 15.385741 8.816732 15.864258 8.950195 C 16.342773 9.083659 16.790363 9.272461 17.207031 9.516602 C 17.623697 9.760742 18.004557 10.055339 18.349609 10.400391 C 18.69466 10.745443 18.989258 11.126303 19.233398 11.542969 C 19.477539 11.959636 19.66634 12.407227 19.799805 12.885742 C 19.933268 13.364258 20 13.860678 20 14.375 C 20 15.14974 19.851887 15.878906 19.555664 16.5625 C 19.259439 17.246094 18.857422 17.841797 18.349609 18.349609 C 17.841797 18.857422 17.246094 19.259439 16.5625 19.555664 C 15.878906 19.851889 15.149739 20 14.375 20 C 13.59375 20 12.861328 19.853516 12.177734 19.560547 C 11.494141 19.267578 10.898438 18.867188 10.390625 18.359375 C 9.882812 17.851562 9.482422 17.255859 9.189453 16.572266 C 8.896484 15.888672 8.75 15.15625 8.75 14.375 Z M 0 13.701172 C 0 13.375651 0.066732 13.064779 0.200195 12.768555 C 0.333659 12.472331 0.512695 12.211914 0.737305 11.987305 C 0.961914 11.762695 1.222331 11.583659 1.518555 11.450195 C 1.814779 11.316732 2.125651 11.25 2.451172 11.25 L 7.900391 11.25 C 7.809244 11.451823 7.722981 11.656901 7.641602 11.865234 C 7.560221 12.073568 7.490234 12.285156 7.431641 12.5 L 2.5 12.5 C 2.324219 12.5 2.161458 12.532553 2.011719 12.597656 C 1.861979 12.662761 1.730143 12.752279 1.616211 12.866211 C 1.502279 12.980144 1.41276 13.111979 1.347656 13.261719 C 1.282552 13.411459 1.25 13.574219 1.25 13.75 C 1.25 14.407553 1.359049 14.986979 1.577148 15.488281 C 1.795247 15.989584 2.091471 16.425781 2.46582 16.796875 C 2.840169 17.167969 3.273112 17.478842 3.764648 17.729492 C 4.256185 17.980143 4.777018 18.180338 5.327148 18.330078 C 5.877278 18.479818 6.438802 18.58724 7.011719 18.652344 C 7.584635 18.717447 8.138021 18.75 8.671875 18.75 C 8.847656 18.977865 9.033203 19.192709 9.228516 19.394531 C 9.423828 19.596354 9.635416 19.785156 9.863281 19.960938 C 9.674479 19.973959 9.488932 19.983725 9.306641 19.990234 C 9.124349 19.996744 8.938802 20 8.75 20 C 7.897135 20 7.042643 19.939779 6.186523 19.819336 C 5.330403 19.698893 4.511719 19.462891 3.730469 19.111328 C 3.157552 18.850912 2.641602 18.543295 2.182617 18.188477 C 1.723633 17.833658 1.333008 17.431641 1.010742 16.982422 C 0.688477 16.533203 0.439453 16.035156 0.263672 15.488281 C 0.087891 14.941406 0 14.345703 0 13.701172 Z M 15 15 L 16.875 15 C 17.04427 15 17.190754 14.938151 17.314453 14.814453 C 17.43815 14.690756 17.5 14.544271 17.5 14.375 C 17.5 14.205729 17.43815 14.059245 17.314453 13.935547 C 17.190754 13.81185 17.04427 13.75 16.875 13.75 L 15 13.75 L 15 11.875 C 14.999999 11.705729 14.93815 11.559245 14.814453 11.435547 C 14.690755 11.31185 14.544271 11.25 14.375 11.25 C 14.205729 11.25 14.059244 11.31185 13.935547 11.435547 C 13.811849 11.559245 13.75 11.705729 13.75 11.875 L 13.75 13.75 L 11.875 13.75 C 11.705729 13.75 11.559244 13.81185 11.435547 13.935547 C 11.311849 14.059245 11.25 14.205729 11.25 14.375 C 11.25 14.544271 11.311849 14.690756 11.435547 14.814453 C 11.559244 14.938151 11.705729 15 11.875 15 L 13.75 15 L 13.75 16.875 C 13.75 17.044271 13.811849 17.190756 13.935547 17.314453 C 14.059244 17.43815 14.205729 17.5 14.375 17.5 C 14.544271 17.5 14.690755 17.43815 14.814453 17.314453 C 14.93815 17.190756 14.999999 17.044271 15 16.875 Z " /> - + diff --git a/Wino.Mail.WinUI/Views/MailRenderingPage.xaml b/Wino.Mail.WinUI/Views/MailRenderingPage.xaml index 733028c6..93808935 100644 --- a/Wino.Mail.WinUI/Views/MailRenderingPage.xaml +++ b/Wino.Mail.WinUI/Views/MailRenderingPage.xaml @@ -179,6 +179,8 @@ + + + + + + + + + + + + + + + + + + + - + + + @@ -65,8 +67,26 @@ CommandParameter="{x:Bind}" IsChecked="{x:Bind IsPrimary, Mode=OneWay}" /> -